using RealTimeGraphEx.Components; using RealTimeGraphEx.Enums; using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.Globalization; using System.Linq; using System.Runtime.ExceptionServices; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Input; using System.Windows.Markup; using System.Windows.Media; using System.Windows.Media.Animation; using System.Windows.Media.Imaging; using System.Windows.Shapes; using System.Windows.Threading; namespace RealTimeGraphEx { #region Delegates public delegate void ZoomCompleteDelegate(Point transformOrigin, double scaleX, double scaleY); public delegate void PanningCompleteDelegate(Point translate); #endregion /// /// Represents an abstract base class for the RealTimeGraphEx. /// [ContentProperty("Components")] public abstract class RealTimeGraphExBase : UserControl, IDisposable { private Rect lastBounds; private Point lastCenter; private bool _loaded; #region Events /// /// Occurs just before rendering the actual visuals on the writeable bitmap. /// public event Action BeforeRenderVisuals; /// /// Occurs after rendering the actual visuals on the writeable bitmap. /// public event Action AfterRenderVisuals; /// /// Occurs when [zooming to]. /// public event ZoomCompleteDelegate ZoomComplete; /// /// Occurs when [panning to]. /// public event PanningCompleteDelegate PanningComplete; /// /// Occurs when the mouse pointer moves while over this element. /// public new event MouseEventHandler MouseMove; /// /// Occurs when the mouse pointer leaves the bounds of this element. /// public new event MouseEventHandler MouseLeave; /// /// Occurs when [minimum maximum changed]. /// public event EventHandler MinMaxChanged; #endregion #region Protected Fields //Visuals internal Grid gridMain; internal Grid gridLinesAndImageWrapperGrid; internal Grid gridBack; internal StackPanel stackLeft; internal StackPanel stackRight; internal double virtualMinimum; internal double virtualMaximum; internal double virtualStart; internal double virtualEnd; protected Image img; internal Grid gridInnerContentWrapper; //protected ScaleTransform scaleTransform; //protected TranslateTransform translateTransform; protected Canvas selectionCanvas; protected Rectangle selectionRectangle; protected Thumb moveThumb; protected bool isSelectionMouseDown; protected bool isScaled; //Multi threading protected Thread pushThread; protected ManualResetEvent _requestTermination; protected ManualResetEvent _terminated; //Plotting protected double xValueCounter; //Resizing protected DispatcherTimer resizeTimer; protected bool isAfterResize; #endregion #region Properties /// /// Gets or sets whether this graph is controlled by a synchronization manager. /// internal bool IsSynced { get; set; } /// /// Gets or sets an extra content which will be placed on top of the graph control. /// public FrameworkElement InnerContent { get { return (FrameworkElement)GetValue(InnerContentProperty); } set { SetValue(InnerContentProperty, value); } } public static readonly DependencyProperty InnerContentProperty = DependencyProperty.Register("InnerContent", typeof(FrameworkElement), typeof(RealTimeGraphExBase), new PropertyMetadata(null, new PropertyChangedCallback(InnerContentChanged))); private static void InnerContentChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { //Add the content control to the inner wrapper grid. (d as RealTimeGraphExBase).gridInnerContentWrapper.Children.Add((d as RealTimeGraphExBase).InnerContent); } /// /// Gets or sets the maximum expected value to be plotted on the graph (default 255). /// public double Maximum { get { return (double)GetValue(MaximumProperty); } set { SetValue(MaximumProperty, value); } } public static readonly DependencyProperty MaximumProperty = DependencyProperty.Register("Maximum", typeof(double), typeof(RealTimeGraphExBase), new PropertyMetadata(255.0, (d, e) => { var control = d as RealTimeGraphExBase; CrossModelChanged(d, e); control.MinMaxChanged?.Invoke(control, new EventArgs()); })); /// /// Gets or sets the minimum expected value to be plotted on the graph (default 0). /// public double Minimum { get { return (double)GetValue(MinimumProperty); } set { SetValue(MinimumProperty, value); } } public static readonly DependencyProperty MinimumProperty = DependencyProperty.Register("Minimum", typeof(double), typeof(RealTimeGraphExBase), new PropertyMetadata(0.0, (d, e) => { var control = d as RealTimeGraphExBase; CrossModelChanged(d, e); control.MinMaxChanged?.Invoke(control, new EventArgs()); })); /// /// Gets or sets the graph refresh rate in milliseconds (default 30, affects performance). /// public int RefreshRate { get { return (int)GetValue(RefreshRateProperty); } set { SetValue(RefreshRateProperty, value); } } public static readonly DependencyProperty RefreshRateProperty = DependencyProperty.Register("RefreshRate", typeof(int), typeof(RealTimeGraphExBase), new PropertyMetadata(30, new PropertyChangedCallback(CrossModelChanged))); /// /// Gets or sets whether the graph will be rendered antialiased (affects performance). /// public bool Antialiased { get { return (bool)GetValue(AntialiasedProperty); } set { SetValue(AntialiasedProperty, value); } } public static readonly DependencyProperty AntialiasedProperty = DependencyProperty.Register("Antialiased", typeof(bool), typeof(RealTimeGraphExBase), new PropertyMetadata(true, new PropertyChangedCallback(CrossModelChanged))); /// /// Gets or sets the collection of graph add-on components. /// public ObservableCollection Components { get { return (ObservableCollection)GetValue(ComponentsProperty); } set { SetValue(ComponentsProperty, value); } } public static readonly DependencyProperty ComponentsProperty = DependencyProperty.Register("Components", typeof(ObservableCollection), typeof(RealTimeGraphExBase), new PropertyMetadata(null, new PropertyChangedCallback(ComponentsChanged))); private static void ComponentsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { (d as RealTimeGraphExBase).OnRenderComponents(); } /// /// Gets or sets a value indicating whether this graph is paused. /// /// /// true if this instance is paused; otherwise, false. /// public bool IsPaused { get { return (bool)GetValue(IsPausedProperty); } set { SetValue(IsPausedProperty, value); } } public static readonly DependencyProperty IsPausedProperty = DependencyProperty.Register("IsPaused", typeof(bool), typeof(RealTimeGraphExBase), new PropertyMetadata(false, new PropertyChangedCallback(CrossModelChanged))); ///// ///// Gets or sets the zoom level for the graph. ///// public double Zoom { get { return (double)GetValue(ZoomProperty); } set { SetValue(ZoomProperty, value); } } public static readonly DependencyProperty ZoomProperty = DependencyProperty.Register("Zoom", typeof(double), typeof(RealTimeGraphExBase), new PropertyMetadata(1.0, new PropertyChangedCallback(ZoomChanged))); private static void ZoomChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var control = d as RealTimeGraphExBase; if (control.ZoomMode != ZoomModeEnum.Manual && control.ZoomMode != ZoomModeEnum.MouseWheel) return; if (control.Zoom < 1) { control.Zoom = 1; return; } if (control.Zoom > 1) { control.gridLinesAndImageWrapperGrid.Cursor = Cursors.SizeAll; } else { control.gridLinesAndImageWrapperGrid.Cursor = Cursors.Arrow; } control.gridLinesAndImageWrapperGrid.RenderTransformOrigin = new Point(0.5, 0.5); //control.scaleTransform.ScaleX = control.Zoom; //control.scaleTransform.ScaleY = control.Zoom; double desiredWidth = control.gridMain.ActualWidth * control.Zoom; double desiredHeight = control.gridMain.ActualHeight * control.Zoom; double desiredLeft = ((desiredWidth - control.gridMain.ActualWidth) / 2) * -1; double desiredTop = ((desiredHeight - control.gridMain.ActualHeight) / 2) * -1; control.gridLinesAndImageWrapperGrid.Width = desiredWidth; control.gridLinesAndImageWrapperGrid.Height = desiredHeight; control.gridLinesAndImageWrapperGrid.Margin = new Thickness(desiredLeft, desiredTop, 0, 0); Point relativePoint = control.gridLinesAndImageWrapperGrid.TransformToAncestor(control.gridMain).Transform(new Point(0, 0)); Rect bounds = control.BoundsRelativeTo(control.gridLinesAndImageWrapperGrid, control.gridMain); if (desiredLeft > 0) { control.gridLinesAndImageWrapperGrid.Margin = new Thickness(0, control.gridLinesAndImageWrapperGrid.Margin.Top, 0, 0); } if (desiredTop > 0) { control.gridLinesAndImageWrapperGrid.Margin = new Thickness(control.gridLinesAndImageWrapperGrid.Margin.Left, 0, 0, 0); } if (desiredWidth < control.gridMain.ActualWidth) { control.gridLinesAndImageWrapperGrid.Margin = new Thickness(0, control.gridLinesAndImageWrapperGrid.Margin.Top, 0, 0); } if (desiredHeight < control.gridMain.ActualHeight) { control.gridLinesAndImageWrapperGrid.Margin = new Thickness(control.gridLinesAndImageWrapperGrid.Margin.Left, 0, 0, 0); } control.OnZoomingComplete(new Point(0, 0), control.Zoom, control.Zoom); } /// /// Gets or sets the zoom mode. /// /// /// The zoom mode. /// public ZoomModeEnum ZoomMode { get { return (ZoomModeEnum)GetValue(ZoomModeProperty); } set { SetValue(ZoomModeProperty, value); } } public static readonly DependencyProperty ZoomModeProperty = DependencyProperty.Register("ZoomMode", typeof(ZoomModeEnum), typeof(RealTimeGraphExBase), new PropertyMetadata(ZoomModeEnum.Selection)); /// /// Gets or sets the mouse value. /// /// /// The mouse value. /// public double MouseValue { get { return (double)GetValue(MouseValueProperty); } set { SetValue(MouseValueProperty, value); } } public static readonly DependencyProperty MouseValueProperty = DependencyProperty.Register("MouseValue", typeof(double), typeof(RealTimeGraphExBase), new PropertyMetadata(0.0)); /// /// Gets or sets the selection fill. /// /// /// The selection fill. /// public Brush SelectionFill { get { return (Brush)GetValue(SelectionFillProperty); } set { SetValue(SelectionFillProperty, value); } } public static readonly DependencyProperty SelectionFillProperty = DependencyProperty.Register("SelectionFill", typeof(Brush), typeof(RealTimeGraphExBase), new PropertyMetadata(new SolidColorBrush() { Color = Colors.Black, Opacity = 0.2 }, new PropertyChangedCallback(SelectionFillChanged))); private static void SelectionFillChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var control = d as RealTimeGraphExBase; control.selectionRectangle.Fill = control.SelectionFill; } /// /// Gets or sets the selection fill. /// /// /// The selection fill. /// public Brush SelectionStroke { get { return (Brush)GetValue(SelectionStrokeProperty); } set { SetValue(SelectionStrokeProperty, value); } } public static readonly DependencyProperty SelectionStrokeProperty = DependencyProperty.Register("SelectionStroke", typeof(Brush), typeof(RealTimeGraphExBase), new PropertyMetadata(Brushes.White, new PropertyChangedCallback(SelectionStrokeChanged))); private static void SelectionStrokeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var control = d as RealTimeGraphExBase; control.selectionRectangle.Stroke = control.SelectionStroke; } public ZoomDirectionEnum ZoomDirection { get { return (ZoomDirectionEnum)GetValue(ZoomDirectionProperty); } set { SetValue(ZoomDirectionProperty, value); } } public static readonly DependencyProperty ZoomDirectionProperty = DependencyProperty.Register("ZoomDirection", typeof(ZoomDirectionEnum), typeof(RealTimeGraphExBase), new PropertyMetadata(ZoomDirectionEnum.Both)); /// /// Gets or sets the maximum points to display on the graph (default 1000). /// public int MaxPoints { get { return (int)GetValue(MaxPointsProperty); } set { SetValue(MaxPointsProperty, value); } } public static readonly DependencyProperty MaxPointsProperty = DependencyProperty.Register("MaxPoints", typeof(int), typeof(RealTimeGraphExBase), new PropertyMetadata(1000, new PropertyChangedCallback(CrossModelChanged))); /// /// Gets or sets a value indicating whether [use automatic range]. /// public bool UseAutoRange { get { return (bool)GetValue(UseAutoRangeProperty); } set { SetValue(UseAutoRangeProperty, value); } } public static readonly DependencyProperty UseAutoRangeProperty = DependencyProperty.Register("UseAutoRange", typeof(bool), typeof(RealTimeGraphExBase), new PropertyMetadata(false, new PropertyChangedCallback(CrossModelChanged))); #endregion #region Cross Thread Fields protected double _height; protected double _width; protected double _mainHeight; protected double _mainWidth; protected double _offSetX; protected double _offSetY; protected int _refreshRate; protected double _maximum; protected double _minimum; protected double _originalMinimum; protected bool _antialiased; protected bool _isPaused; protected bool _disableRendering; protected bool _useAutoRange; #endregion #region Constructors /// /// Initializes a new instance of RealTimeGraphExBase /// public RealTimeGraphExBase() { this.Loaded += OnGraphLoaded; this.SizeChanged += OnSizeChanged; resizeTimer = new DispatcherTimer(); resizeTimer.Interval = TimeSpan.FromMilliseconds(150); resizeTimer.IsEnabled = false; resizeTimer.Tick += OnResizeEnd; Components = new ObservableCollection(); //Uri NewTheme = new Uri(@"/RealTimeGraphEx;component/Resources/Resources.xaml", UriKind.Relative); //ResourceDictionary dictionary = (ResourceDictionary)Application.LoadComponent(NewTheme); //Application.Current.Resources.MergedDictionaries.Add(dictionary); Initialize(); } #endregion #region Virtual Methods /// /// Enabled/Disabled the rendering of the graph's image. /// protected virtual void ChangeRenderMode(bool enabled) { _disableRendering = !enabled; } /// /// Raises the BeforeRenderVisuals event. /// /// protected virtual void OnBeforeRenderingVisuals(WriteableBitmap bmp, double scaleFactor) { if (BeforeRenderVisuals != null) BeforeRenderVisuals(bmp, scaleFactor); } /// /// Raises the AfterRenderVisuals event. /// /// protected virtual void OnAfterRenderingVisuals(WriteableBitmap bmp, double scaleFactor) { if (AfterRenderVisuals != null) AfterRenderVisuals(bmp, scaleFactor); } /// /// Override this method to apply logic when graph resize is complete. /// /// /// protected virtual void OnResizeEnd(object sender, EventArgs e) { resizeTimer.IsEnabled = false; resizeTimer.Stop(); OnSetCrossThreadFields(); isAfterResize = true; } /// /// Override this method to apply logic while graph is resizing. /// /// /// protected virtual void OnSizeChanged(object sender, SizeChangedEventArgs e) { resizeTimer.IsEnabled = true; resizeTimer.Stop(); resizeTimer.Start(); } /// /// Initializes the control. /// protected virtual void Initialize() { //Initialize the main grid gridMain = new Grid() { ClipToBounds = true }; gridMain.ColumnDefinitions.Add(new ColumnDefinition() { Width = GridLength.Auto }); gridMain.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(1, GridUnitType.Star) }); gridMain.ColumnDefinitions.Add(new ColumnDefinition() { Width = GridLength.Auto }); //Initialize the grid lines and image wrapper grid. gridLinesAndImageWrapperGrid = new Grid() { Background = Brushes.Transparent }; //Initialize the grid lines grid. gridBack = new Grid(); //Initialize the graph image. img = new Image() { Stretch = Stretch.None, HorizontalAlignment = HorizontalAlignment.Left, VerticalAlignment = VerticalAlignment.Stretch }; //Initialize the Y Axis grid. stackLeft = new StackPanel() { HorizontalAlignment = HorizontalAlignment.Left, Orientation = Orientation.Horizontal, FlowDirection = System.Windows.FlowDirection.LeftToRight }; //Initializing the X Axis grid. stackRight = new StackPanel() { HorizontalAlignment = HorizontalAlignment.Right, Orientation = Orientation.Horizontal, FlowDirection = System.Windows.FlowDirection.RightToLeft }; //Initialize the inner content wrapper. gridInnerContentWrapper = new Grid(); //Add the main grid to the control. this.Content = gridMain; //Add the wrapper grid to the main grid. gridMain.Children.Add(gridLinesAndImageWrapperGrid); //Add the grid lines grid to the wrapper. gridLinesAndImageWrapperGrid.Children.Add(gridBack); //Add the graph image to the wrapper. gridMain.Children.Add(img); img.IsHitTestVisible = false; Grid.SetColumnSpan(img, 3); //Add the left grid to the main grid. gridMain.Children.Add(stackLeft); //Add the right grid to the main grid. gridMain.Children.Add(stackRight); gridMain.Children.Add(gridInnerContentWrapper); Grid.SetColumn(stackLeft, 0); Grid.SetColumnSpan(stackLeft, 1); Grid.SetColumn(stackRight, 2); Grid.SetColumnSpan(stackRight, 1); Grid.SetColumn(gridLinesAndImageWrapperGrid, 1); Grid.SetColumnSpan(gridLinesAndImageWrapperGrid, 1); Grid.SetColumn(gridInnerContentWrapper, 0); Grid.SetColumnSpan(gridInnerContentWrapper, 3); //Set size changed event to get the exact size of image grid! gridLinesAndImageWrapperGrid.SizeChanged += OnImageGridSizeChanged; //Zooming //scaleTransform = new ScaleTransform(1, 1); //translateTransform = new TranslateTransform(0, 0); //TransformGroup group = new TransformGroup(); //group.Children.Add(scaleTransform); //group.Children.Add(translateTransform); //gridLinesAndImageWrapperGrid.RenderTransform = group; gridLinesAndImageWrapperGrid.PreviewMouseWheel += gridLinesAndImageWrapperGrid_PreviewMouseWheel; moveThumb = new Thumb() { HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch, VerticalAlignment = System.Windows.VerticalAlignment.Stretch, Opacity = 0 }; if (!gridLinesAndImageWrapperGrid.Children.Contains(moveThumb)) { gridLinesAndImageWrapperGrid.Children.Add(moveThumb); moveThumb.DragDelta += moveThumb_DragDelta; } selectionCanvas = new Canvas() { Background = null, HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch, VerticalAlignment = System.Windows.VerticalAlignment.Stretch }; Panel.SetZIndex(moveThumb, 9999); Grid.SetColumnSpan(selectionCanvas, 3); gridMain.Children.Add(selectionCanvas); selectionRectangle = new Rectangle() { Fill = SelectionFill, Stroke = SelectionStroke }; gridLinesAndImageWrapperGrid.PreviewMouseDown += gridLinesAndImageWrapperGrid_PreviewMouseDown; selectionCanvas.MouseMove += selectionCanvas_MouseMove; selectionCanvas.PreviewMouseUp += selectionCanvas_PreviewMouseUp; gridLinesAndImageWrapperGrid.PreviewMouseLeftButtonDown += gridLinesAndImageWrapperGrid_PreviewMouseLeftButtonDown; gridLinesAndImageWrapperGrid.MouseMove += OnMouseMove; gridLinesAndImageWrapperGrid.MouseLeave += OnMouseLeave; this.SizeChanged += (x, y) => { if (isScaled) { ResetZoom(); }; }; } /// /// Resets the graph zooming. /// public virtual void ResetZoom() { if (!isScaled) return; isScaled = false; gridLinesAndImageWrapperGrid.RenderTransformOrigin = new Point(0.5, 0.5); DoubleAnimation aniX = new DoubleAnimation(); aniX.Duration = new Duration(TimeSpan.FromSeconds(0.1)); aniX.To = gridMain.ActualWidth; DoubleAnimation aniY = new DoubleAnimation(); aniY.Duration = new Duration(TimeSpan.FromSeconds(0.1)); aniY.To = gridMain.ActualHeight; ThicknessAnimation marginAni = new ThicknessAnimation(); marginAni.Duration = new Duration(TimeSpan.FromSeconds(0.1)); marginAni.To = new Thickness(0, 0, 0, 0); if (!double.IsNaN(gridLinesAndImageWrapperGrid.Width)) { gridLinesAndImageWrapperGrid.BeginAnimation(Grid.WidthProperty, aniX); } if (!double.IsNaN(gridLinesAndImageWrapperGrid.Height)) { gridLinesAndImageWrapperGrid.BeginAnimation(Grid.HeightProperty, aniY); } aniY.CurrentTimeInvalidated += (x, y) => { ApplyVirtualMaximumMinimum(); }; marginAni.Completed += (x, y) => { if (!double.IsNaN(gridLinesAndImageWrapperGrid.Width)) { gridLinesAndImageWrapperGrid.BeginAnimation(Grid.WidthProperty, null); } if (!double.IsNaN(gridLinesAndImageWrapperGrid.Height)) { gridLinesAndImageWrapperGrid.BeginAnimation(Grid.HeightProperty, null); } gridLinesAndImageWrapperGrid.Width = double.NaN; gridLinesAndImageWrapperGrid.Height = double.NaN; gridLinesAndImageWrapperGrid.BeginAnimation(Grid.MarginProperty, null); gridLinesAndImageWrapperGrid.Margin = new Thickness(0, 0, 0, 0); ApplyVirtualMaximumMinimum(); Thread t = new Thread(() => { Thread.Sleep(100); this.Dispatcher.BeginInvoke(new Action(() => { OnZoomingComplete(new Point(0.5, 0.5), gridLinesAndImageWrapperGrid.ActualWidth, gridLinesAndImageWrapperGrid.ActualHeight); }), DispatcherPriority.Background); }); t.Start(); }; gridLinesAndImageWrapperGrid.BeginAnimation(Grid.MarginProperty, marginAni); gridLinesAndImageWrapperGrid.Cursor = Cursors.Arrow; gridLinesAndImageWrapperGrid.PreviewMouseDown += gridLinesAndImageWrapperGrid_PreviewMouseDown; } /// /// Override this method to apply extra logic when the grid containing the graph and grid lines changes its size. /// /// /// protected virtual void OnImageGridSizeChanged(object sender, SizeChangedEventArgs e) { OnSetCrossThreadFields(); } /// /// Occurs after the loaded graph event. /// /// Graph instance. /// Arguments protected virtual void OnGraphLoaded(object sender, RoutedEventArgs e) { if (!_loaded) { _loaded = true; if (InnerContent != null) { gridInnerContentWrapper.Children.Clear(); gridInnerContentWrapper.Children.Add(InnerContent); } OnRenderComponents(); OnSetCrossThreadFields(); } } /// /// Set the RealTimeGraphCrossThreadModel properties used by the graph thread. /// protected virtual void OnSetCrossThreadFields() { this.Dispatcher.Invoke(() => { _antialiased = Antialiased; _height = gridLinesAndImageWrapperGrid.ActualHeight; _width = gridLinesAndImageWrapperGrid.ActualWidth; _mainWidth = gridMain.ActualWidth; _mainHeight = gridMain.ActualHeight; _maximum = Maximum; _minimum = Minimum; _originalMinimum = Minimum; _refreshRate = RefreshRate; _isPaused = IsPaused; virtualMinimum = Minimum; virtualMaximum = Maximum; virtualStart = 0; virtualEnd = gridMain.ActualWidth; _useAutoRange = UseAutoRange; RenderOptions.SetEdgeMode(this, _antialiased ? EdgeMode.Unspecified : EdgeMode.Aliased); }, DispatcherPriority.Send); } /// /// Override this method to apply extra logic for clearing the graph. /// protected virtual void OnClearGraph() { } /// /// Override this method to render the graph image. /// protected internal virtual void OnRenderGraph() { } /// /// Override this method to set the cross thread model properties. /// /// protected virtual void OnGraphPropertiesChanged(DependencyPropertyChangedEventArgs e) { OnSetCrossThreadFields(); } /// /// Convert the specified integer to graph Y position. /// /// /// protected virtual double ConvertYToImageY(double value) { double valuePrecentage = ((((value - _minimum) * 100) / (_maximum - _minimum)) * _height) / 100; return valuePrecentage; } /// /// Convert the specified integer to graph X position. /// /// /// protected virtual double ConvertYToImageX(double value) { double valuePrecentage = ((((value - _minimum) * 100) / (_maximum - _minimum)) * _width) / 100; return valuePrecentage; } /// /// Convert the specified integer to graph flipped Y position. /// /// /// protected virtual double ConvertYToImageYFliped(double value) { double valuePrecentage = ConvertYToImageY(value); valuePrecentage = _height - valuePrecentage; //Flip return valuePrecentage; } /// /// Renders the collection of graph components. /// protected virtual void OnRenderComponents() { if (Components != null) { foreach (var component in Components) { component.Graph = this; component.Render(this); if (component.Location == ComponentLocationEnum.Left) { if (!stackLeft.Children.Contains(component)) { stackLeft.Children.Add(component); } } else if (component.Location == ComponentLocationEnum.Right) { if (!stackRight.Children.Contains(component)) { stackRight.Children.Add(component); } } else if (component.Location == ComponentLocationEnum.Back) { if (!gridBack.Children.Contains(component)) { gridBack.Children.Add(component); } } else if (component.Location == ComponentLocationEnum.Front) { if (!gridInnerContentWrapper.Children.Contains(component)) { gridInnerContentWrapper.Children.Add(component); } } } } } /// /// Sets the current state of the graph. /// /// if set to true [paused]. protected virtual void SetPaused(bool paused) { this.Dispatcher.Invoke(() => { IsPaused = paused; }); } /// /// Called when [zooming to]. /// /// The transform origin. /// The scale markerPosition. /// The scale y. protected virtual void OnZoomingComplete(Point transformOrigin, double scaleX, double scaleY) { if (ZoomComplete != null) ZoomComplete(transformOrigin, scaleX, scaleY); } /// /// Called when [panning to]. /// /// The translate. protected virtual void OnPanningComplete(Point translate) { if (PanningComplete != null) PanningComplete(translate); } /// /// Converts the y position to value. /// /// The Y Position. /// The Y Value protected virtual double ConvertYPositionToValue(double y) { double valuePrecentage = (y * 100) / gridLinesAndImageWrapperGrid.ActualHeight; valuePrecentage = (valuePrecentage * (_maximum - _minimum)) / 100; valuePrecentage = _minimum + (_maximum - _minimum) - valuePrecentage; //Flip return valuePrecentage; } /// /// Add an IRealTimeGraphComponent instance to the components collection. /// /// internal virtual void ApplyComponent(FrameworkElement component) { if (!stackLeft.Children.Contains(component) && !stackLeft.Children.Contains(component)) { if (component.HorizontalAlignment == System.Windows.HorizontalAlignment.Right) { stackRight.Children.Add(component); } else { stackLeft.Children.Add(component); } } } /// /// Called when [mouse move]. /// /// The sender. /// The instance containing the event data. protected virtual void OnMouseMove(object sender, MouseEventArgs e) { MouseValue = ConvertYPositionToValue(e.GetPosition(gridLinesAndImageWrapperGrid).Y); if (MouseMove != null) MouseMove(this, e); } /// /// Called when [mouse leave]. /// /// The sender. /// The instance containing the event data. protected virtual void OnMouseLeave(object sender, MouseEventArgs e) { if (MouseLeave != null) MouseLeave(this, e); } /// /// Normalizes the value. /// /// The value. protected virtual void NormalizeValue(ref double value) { if (value > _maximum) { value = _maximum; } else if (value < _originalMinimum) { value = _originalMinimum; } } /// /// Override to apply extra logic when dragging on zoom mode. /// /// The instance containing the event data. protected virtual void OnDragging(DragDeltaEventArgs e) { } #endregion #region Protected Methods /// /// Clears the graph. /// protected void ClearGraph() { StopPushThread(); xValueCounter = 0; OnSetCrossThreadFields(); OnClearGraph(); if (img != null) { WriteableBitmap bmp = BitmapFactory.New((int)_width, (int)_height); bmp.Clear(Colors.Transparent); bmp.Freeze(); img.Dispatcher.BeginInvoke(new Action(() => { img.Source = bmp; }), System.Windows.Threading.DispatcherPriority.Send); } } /// /// Starts the "push" thread. /// protected void StartPushThread() { if (pushThread == null && !IsSynced) { _requestTermination = new ManualResetEvent(false); _terminated = new ManualResetEvent(false); pushThread = new Thread(PushDataThreadMethod); pushThread.IsBackground = true; pushThread.Start(); } } /// /// Stops the "push" thread. /// protected void StopPushThread() { if (pushThread != null) { _requestTermination.Set(); _terminated.WaitOne(50); pushThread = null; } } /// /// Draw the specified polygon vectorsCollection on the graph. The drawing will be anti-aliased if specified in the cross model fields. /// [DebuggerStepThrough] [DebuggerHidden] protected void DrawPolyline(WriteableBitmap bmp, int[] points, Color stroke) { try { if (points.Length > 0) { if (_antialiased) { bmp.DrawPolylineAa(points, stroke); } else { bmp.DrawPolyline(points, stroke); } } } catch { Debug.WriteLine("[RealTimeGraphEx] [DrawPolyline] Error encountered while trying to Draw on bitmap."); } } /// /// Forces events update. /// public static void DoEvents() { Application.Current.Dispatcher.Invoke(DispatcherPriority.Background, new Action(delegate { })); } #endregion #region Private Methods /// /// The push thread method. /// [HandleProcessCorruptedStateExceptions] private void PushDataThreadMethod() { while (!_requestTermination.WaitOne(0)) { try { OnRenderGraph(); } catch { } Thread.Sleep(_refreshRate); } _terminated.Set(); } private Rect BoundsRelativeTo(FrameworkElement element, Visual relativeTo) { return element.TransformToVisual(relativeTo) .TransformBounds(LayoutInformation.GetLayoutSlot(element)); } private void ApplyVirtualMaximumMinimum() { var b = new Rect(gridLinesAndImageWrapperGrid.Margin.Left, gridLinesAndImageWrapperGrid.Margin.Top, gridLinesAndImageWrapperGrid.Width, gridLinesAndImageWrapperGrid.Height); double maxDecreasePrecentage = (Math.Abs(b.Top) * 100) / gridMain.ActualHeight; double minIncreasePrecentage = ((b.Bottom - gridMain.ActualHeight) * 100) / gridMain.ActualHeight; virtualMaximum = ConvertYPositionToValue(Math.Abs(b.Top)); virtualMinimum = ConvertYPositionToValue(b.Height - Math.Abs(b.Height - (gridMain.ActualHeight - b.Top))); virtualStart = Math.Abs(b.Left); virtualEnd = virtualStart + gridMain.ActualWidth; _offSetX = gridLinesAndImageWrapperGrid.Margin.Left; _offSetY = gridLinesAndImageWrapperGrid.Margin.Top; } #endregion #region Public Methods /// /// Gets the size of the graph render. /// /// public Rect GetGraphRenderBounds() { var b = BoundsRelativeTo(gridLinesAndImageWrapperGrid, gridMain); return b; } public void Clear() { ClearGraph(); } public void RenderComponents() { OnRenderComponents(); } #endregion #region Static Methods protected static void CrossModelChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { (d as RealTimeGraphExBase).OnGraphPropertiesChanged(e); } #endregion #region Event Handlers /// /// Handles the PreviewMouseWheel event of the gridLinesAndImageWrapperGrid control. /// /// The source of the event. /// The instance containing the event data. protected void gridLinesAndImageWrapperGrid_PreviewMouseWheel(object sender, MouseWheelEventArgs e) { if (ZoomMode == ZoomModeEnum.MouseWheel) { gridLinesAndImageWrapperGrid.PreviewMouseDown -= gridLinesAndImageWrapperGrid_PreviewMouseDown; Zoom += (e.Delta < 0 ? -0.1 : 0.1); } else if (ZoomMode == ZoomModeEnum.Selection) { var center = new Point(e.GetPosition(gridLinesAndImageWrapperGrid).X, e.GetPosition(gridLinesAndImageWrapperGrid).Y); int zoomFactor = 20; double width = gridLinesAndImageWrapperGrid.ActualWidth + (e.Delta < 0 ? zoomFactor * -1 : zoomFactor); double height = gridLinesAndImageWrapperGrid.ActualHeight + (e.Delta < 0 ? zoomFactor * -1 : zoomFactor); double left = gridLinesAndImageWrapperGrid.Margin.Left; double top = gridLinesAndImageWrapperGrid.Margin.Top; bool reset = true; if (ZoomDirection != ZoomDirectionEnum.Y) { if (width > gridMain.ActualWidth) { gridLinesAndImageWrapperGrid.Width = width; reset = false; } } if (ZoomDirection != ZoomDirectionEnum.X) { if (height > gridMain.ActualHeight) { gridLinesAndImageWrapperGrid.Height = height; reset = false; } } double factorX = zoomFactor / (gridLinesAndImageWrapperGrid.ActualWidth / center.X); double factorY = zoomFactor / (gridLinesAndImageWrapperGrid.ActualHeight / center.Y); if (ZoomDirection != ZoomDirectionEnum.Y) { if (gridLinesAndImageWrapperGrid.Margin.Left + (e.Delta < 0 ? factorX : -factorX) < 0) { left = gridLinesAndImageWrapperGrid.Margin.Left + (e.Delta < 0 ? factorX : -factorX); reset = false; } } if (ZoomDirection != ZoomDirectionEnum.X) { if (gridLinesAndImageWrapperGrid.Margin.Top + (e.Delta < 0 ? factorY : -factorY) < 0) { top = gridLinesAndImageWrapperGrid.Margin.Top + (e.Delta < 0 ? factorY : -factorY); reset = false; } } gridLinesAndImageWrapperGrid.Margin = new Thickness( left, top, gridLinesAndImageWrapperGrid.Margin.Right, gridLinesAndImageWrapperGrid.Margin.Bottom); OnZoomingComplete(lastCenter, gridLinesAndImageWrapperGrid.Width, gridLinesAndImageWrapperGrid.Height); ApplyVirtualMaximumMinimum(); if (reset) { ResetZoom(); gridLinesAndImageWrapperGrid.PreviewMouseDown += gridLinesAndImageWrapperGrid_PreviewMouseDown; isScaled = false; } else { gridLinesAndImageWrapperGrid.PreviewMouseDown -= gridLinesAndImageWrapperGrid_PreviewMouseDown; gridLinesAndImageWrapperGrid.Cursor = Cursors.SizeAll; isScaled = true; } } } /// /// Handles the DragDelta event of the moveThumb control. /// /// The source of the event. /// The instance containing the event data. protected void moveThumb_DragDelta(object sender, DragDeltaEventArgs e) { if (ZoomMode == ZoomModeEnum.None) return; Point relativePoint = gridLinesAndImageWrapperGrid.TransformToAncestor(gridMain).Transform(new Point(0, 0)); Rect bounds = BoundsRelativeTo(gridLinesAndImageWrapperGrid, gridMain); double right = (gridLinesAndImageWrapperGrid.ActualWidth + gridLinesAndImageWrapperGrid.Margin.Left); double bottom = (gridLinesAndImageWrapperGrid.ActualHeight + gridLinesAndImageWrapperGrid.Margin.Top); if (gridLinesAndImageWrapperGrid.Margin.Left + e.HorizontalChange <= 0 && right + e.HorizontalChange >= gridMain.ActualWidth) { gridLinesAndImageWrapperGrid.Margin = new Thickness(gridLinesAndImageWrapperGrid.Margin.Left + e.HorizontalChange, gridLinesAndImageWrapperGrid.Margin.Top, 0, 0); //translateTransform.X += e.HorizontalChange; OnPanningComplete(new Point(gridLinesAndImageWrapperGrid.Margin.Left, gridLinesAndImageWrapperGrid.Margin.Top)); ApplyVirtualMaximumMinimum(); } if (gridLinesAndImageWrapperGrid.Margin.Top + e.VerticalChange <= 0 && bottom + e.VerticalChange >= gridMain.ActualHeight) { gridLinesAndImageWrapperGrid.Margin = new Thickness(gridLinesAndImageWrapperGrid.Margin.Left, gridLinesAndImageWrapperGrid.Margin.Top + e.VerticalChange, 0, 0); //translateTransform.Y += e.VerticalChange; OnPanningComplete(new Point(gridLinesAndImageWrapperGrid.Margin.Left, gridLinesAndImageWrapperGrid.Margin.Top)); ApplyVirtualMaximumMinimum(); } OnDragging(e); } /// /// Handles the PreviewMouseLeftButtonDown event of the gridLinesAndImageWrapperGrid control. /// /// The source of the event. /// The instance containing the event data. protected void gridLinesAndImageWrapperGrid_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e) { if (ZoomMode != ZoomModeEnum.Selection) return; if (e.ClickCount == 2) { ResetZoom(); } } /// /// /// protected void selectionCanvas_PreviewMouseUp(object sender, MouseButtonEventArgs e) { if (ZoomMode != ZoomModeEnum.Selection) return; var bounds = new Rect(Canvas.GetLeft(selectionRectangle), Canvas.GetTop(selectionRectangle), selectionRectangle.Width, selectionRectangle.Height); selectionCanvas.Children.Remove(selectionRectangle); isSelectionMouseDown = false; selectionCanvas.Background = null; if (selectionRectangle.Width <= 5 || selectionRectangle.Height <= 5) return; Point center = new Point(bounds.Left * (gridLinesAndImageWrapperGrid.ActualWidth / bounds.Width), bounds.Top * (gridLinesAndImageWrapperGrid.ActualHeight / bounds.Height)); gridLinesAndImageWrapperGrid.RenderTransformOrigin = center; lastBounds = bounds; lastCenter = center; double scaleX = gridLinesAndImageWrapperGrid.ActualWidth; double scaleY = gridLinesAndImageWrapperGrid.ActualHeight; if (ZoomDirection != ZoomDirectionEnum.Y) { scaleX = gridLinesAndImageWrapperGrid.ActualWidth * (gridLinesAndImageWrapperGrid.ActualWidth / bounds.Width); } if (ZoomDirection != ZoomDirectionEnum.X) { scaleY = gridLinesAndImageWrapperGrid.ActualHeight * (gridLinesAndImageWrapperGrid.ActualHeight / bounds.Height); } isScaled = true; DoubleAnimation aniX = new DoubleAnimation(); aniX.Duration = new Duration(TimeSpan.FromSeconds(0.1)); aniX.To = scaleX; DoubleAnimation aniY = new DoubleAnimation(); aniY.Duration = new Duration(TimeSpan.FromSeconds(0.1)); aniY.To = scaleY; ThicknessAnimation marginAni = new ThicknessAnimation(); marginAni.Duration = new Duration(TimeSpan.FromSeconds(0.1)); marginAni.To = new Thickness(center.X * -1, center.Y * -1, 0, 0); aniY.CurrentTimeInvalidated += (x, y) => { ApplyVirtualMaximumMinimum(); }; aniY.Completed += (x, y) => { gridLinesAndImageWrapperGrid.BeginAnimation(Grid.WidthProperty, null); gridLinesAndImageWrapperGrid.BeginAnimation(Grid.HeightProperty, null); gridLinesAndImageWrapperGrid.Width = scaleX; gridLinesAndImageWrapperGrid.Height = scaleY; gridLinesAndImageWrapperGrid.BeginAnimation(Grid.MarginProperty, null); double marginLeft = 0; double marginTop = 0; if (ZoomDirection != ZoomDirectionEnum.Y) { marginLeft = center.X * -1; } if (ZoomDirection != ZoomDirectionEnum.X) { marginTop = center.Y * -1; } gridLinesAndImageWrapperGrid.Margin = new Thickness(marginLeft, marginTop, 0, 0); OnZoomingComplete(center, scaleX, scaleY); ApplyVirtualMaximumMinimum(); }; gridLinesAndImageWrapperGrid.Width = gridLinesAndImageWrapperGrid.ActualWidth; gridLinesAndImageWrapperGrid.Height = gridLinesAndImageWrapperGrid.ActualHeight; gridLinesAndImageWrapperGrid.BeginAnimation(Grid.MarginProperty, marginAni); gridLinesAndImageWrapperGrid.BeginAnimation(Grid.WidthProperty, aniX); gridLinesAndImageWrapperGrid.BeginAnimation(Grid.HeightProperty, aniY); gridLinesAndImageWrapperGrid.Cursor = Cursors.SizeAll; gridLinesAndImageWrapperGrid.PreviewMouseDown -= gridLinesAndImageWrapperGrid_PreviewMouseDown; } /// /// Handles the MouseMove event of the selectionCanvas control. /// /// The source of the event. /// The instance containing the event data. protected void selectionCanvas_MouseMove(object sender, MouseEventArgs e) { if (isSelectionMouseDown) { double width = e.GetPosition(selectionCanvas).X - Canvas.GetLeft(selectionRectangle); double height = e.GetPosition(selectionCanvas).Y - Canvas.GetTop(selectionRectangle); if (width > 0) { selectionRectangle.Width = width; } if (height > 0) { selectionRectangle.Height = height; } } } /// /// Handles the PreviewMouseDown event of the gridLinesAndImageWrapperGrid control. /// /// The source of the event. /// The instance containing the event data. protected void gridLinesAndImageWrapperGrid_PreviewMouseDown(object sender, MouseButtonEventArgs e) { if (ZoomMode != ZoomModeEnum.Selection) return; selectionCanvas.Children.Remove(selectionRectangle); selectionCanvas.Background = Brushes.Transparent; selectionRectangle.Width = 0; selectionRectangle.Height = 0; selectionCanvas.Children.Add(selectionRectangle); Canvas.SetLeft(selectionRectangle, e.GetPosition(selectionCanvas).X); Canvas.SetTop(selectionRectangle, e.GetPosition(selectionCanvas).Y); isSelectionMouseDown = true; DoEvents(); } #endregion #region IDisposable Members /// /// Disposes the current instance. /// public void Dispose() { if (pushThread != null) { StopPushThread(); } } #endregion } }