Files
DynamicDataDisplay/Viewport2D.cs
2024-02-23 00:46:06 -05:00

612 lines
21 KiB
C#

using System;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Windows;
using System.Windows.Data;
using Microsoft.Research.DynamicDataDisplay.Common.Auxiliary;
using Microsoft.Research.DynamicDataDisplay.ViewportRestrictions;
using Microsoft.Research.DynamicDataDisplay.Common;
using System.Windows.Threading;
namespace Microsoft.Research.DynamicDataDisplay
{
/// <summary>
/// Viewport2D provides virtual coordinates.
/// </summary>
public partial class Viewport2D : DependencyObject
{
private readonly Plotter2D plotter;
internal Plotter2D Plotter2D
{
get { return plotter; }
}
private readonly FrameworkElement hostElement;
internal FrameworkElement HostElement
{
get { return hostElement; }
}
protected internal Viewport2D(FrameworkElement host, Plotter2D plotter)
{
hostElement = host;
host.ClipToBounds = true;
host.SizeChanged += OnHostElementSizeChanged;
this.plotter = plotter;
plotter.Children.CollectionChanged += OnPlotterChildrenChanged;
restrictions = new RestrictionCollection(this);
restrictions.Add(new MinimalSizeRestriction());
restrictions.CollectionChanged += restrictions_CollectionChanged;
fitToViewRestrictions = new RestrictionCollection(this);
fitToViewRestrictions.CollectionChanged += fitToViewRestrictions_CollectionChanged;
readonlyContentBoundsHosts = new ReadOnlyObservableCollection<DependencyObject>(contentBoundsHosts);
UpdateVisible();
UpdateTransform();
}
private void OnHostElementSizeChanged(object sender, SizeChangedEventArgs e)
{
SetValue(OutputPropertyKey, new Rect(e.NewSize));
CoerceValue(VisibleProperty);
}
private void fitToViewRestrictions_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (IsFittedToView)
{
CoerceValue(VisibleProperty);
}
}
private void restrictions_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
CoerceValue(VisibleProperty);
}
private static void OnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
Viewport2D viewport = (Viewport2D)d;
viewport.UpdateTransform();
viewport.RaisePropertyChangedEvent(e);
}
public BindingExpressionBase SetBinding(DependencyProperty property, BindingBase binding)
{
return BindingOperations.SetBinding(this, property, binding);
}
/// <summary>
/// Forces viewport to go to fit to view mode - clears locally set value of <see cref="Visible"/> property
/// and sets it during the coercion process to a value of united content bounds of all charts inside of <see cref="Plotter"/>.
/// </summary>
public void FitToView()
{
if (!IsFittedToView)
{
ClearValue(VisibleProperty);
CoerceValue(VisibleProperty);
}
}
/// <summary>
/// Gets a value indicating whether Viewport is fitted to view.
/// </summary>
/// <value>
/// <c>true</c> if Viewport is fitted to view; otherwise, <c>false</c>.
/// </value>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public bool IsFittedToView
{
get { return ReadLocalValue(VisibleProperty) == DependencyProperty.UnsetValue; }
}
// DispatcherOperation updateVisibleOperation;
internal void UpdateVisible()
{
//if (updateVisibleOperation == null)
//{
// updateVisibleOperation = Dispatcher.BeginInvoke(() => UpdateVisible(), DispatcherPriority.Normal);
// return;
//}
//updateVisibleOperation = Dispatcher.BeginInvoke(() =>
//{
// updateVisibleOperation = null;
if (IsFittedToView)
{
CoerceValue(VisibleProperty);
}
//}, DispatcherPriority.Normal);
}
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[EditorBrowsable(EditorBrowsableState.Never)]
public Plotter2D Plotter
{
get { return plotter; }
}
private readonly RestrictionCollection restrictions;
/// <summary>
/// Gets the collection of <see cref="ViewportRestriction"/>s that are applied each time <see cref="Visible"/> is updated.
/// </summary>
/// <value>The restrictions.</value>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
public RestrictionCollection Restrictions
{
get { return restrictions; }
}
private readonly RestrictionCollection fitToViewRestrictions;
/// <summary>
/// Gets the collection of <see cref="ViewportRestriction"/>s that are applied only when Viewport is fitted to view.
/// </summary>
/// <value>The fit to view restrictions.</value>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
public RestrictionCollection FitToViewRestrictions
{
get { return fitToViewRestrictions; }
}
#region Output property
/// <summary>
/// Gets the rectangle in screen coordinates that is output.
/// </summary>
/// <value>The output.</value>
public Rect Output
{
get { return (Rect)GetValue(OutputProperty); }
}
[SuppressMessage("Microsoft.Security", "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes")]
private static readonly DependencyPropertyKey OutputPropertyKey = DependencyProperty.RegisterReadOnly(
"Output",
typeof(Rect),
typeof(Viewport2D),
new FrameworkPropertyMetadata(new Rect(0, 0, 1, 1), OnPropertyChanged));
/// <summary>
/// Identifies the <see cref="Output"/> dependency property.
/// </summary>
public static readonly DependencyProperty OutputProperty = OutputPropertyKey.DependencyProperty;
#endregion
#region UnitedContentBounds property
/// <summary>
/// Gets the united content bounds of all the charts.
/// </summary>
/// <value>The content bounds.</value>
public DataRect UnitedContentBounds
{
get { return (DataRect)GetValue(UnitedContentBoundsProperty); }
internal set { SetValue(UnitedContentBoundsProperty, value); }
}
/// <summary>
/// Identifies the <see cref="UnitedContentBounds"/> dependency property.
/// </summary>
public static readonly DependencyProperty UnitedContentBoundsProperty = DependencyProperty.Register(
"UnitedContentBounds",
typeof(DataRect),
typeof(Viewport2D),
new FrameworkPropertyMetadata(DataRect.Empty, OnUnitedContentBoundsChanged));
private static void OnUnitedContentBoundsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
Viewport2D owner = (Viewport2D)d;
owner.ContentBoundsChanged.Raise(owner);
}
public event EventHandler ContentBoundsChanged;
#endregion
#region Visible property
/// <summary>
/// Gets or sets the visible rectangle.
/// </summary>
/// <value>The visible.</value>
public DataRect Visible
{
get { return (DataRect)GetValue(VisibleProperty); }
set { SetValue(VisibleProperty, value); }
}
/// <summary>
/// Identifies the <see cref="Visible"/> dependency property.
/// </summary>
public static readonly DependencyProperty VisibleProperty =
DependencyProperty.Register("Visible", typeof(DataRect), typeof(Viewport2D),
new FrameworkPropertyMetadata(
new DataRect(0, 0, 1, 1),
OnPropertyChanged,
OnCoerceVisible),
ValidateVisibleCallback);
private static bool ValidateVisibleCallback(object value)
{
DataRect rect = (DataRect)value;
return !rect.IsNaN();
}
private void UpdateContentBoundsHosts()
{
contentBoundsHosts.Clear();
foreach (var item in plotter.Children)
{
DependencyObject dependencyObject = item as DependencyObject;
if (dependencyObject != null)
{
bool hasNonEmptyBounds = !Viewport2D.GetContentBounds(dependencyObject).IsEmpty;
if (hasNonEmptyBounds && Viewport2D.GetIsContentBoundsHost(dependencyObject))
{
contentBoundsHosts.Add(dependencyObject);
}
}
}
UpdateVisible();
}
private readonly ObservableCollection<DependencyObject> contentBoundsHosts = new ObservableCollection<DependencyObject>();
private readonly ReadOnlyObservableCollection<DependencyObject> readonlyContentBoundsHosts;
/// <summary>
/// Gets the collection of all charts that can has its own content bounds.
/// </summary>
/// <value>The content bounds hosts.</value>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public ReadOnlyObservableCollection<DependencyObject> ContentBoundsHosts
{
get { return readonlyContentBoundsHosts; }
}
private bool useApproximateContentBoundsComparison = true;
/// <summary>
/// Gets or sets a value indicating whether to use approximate content bounds comparison.
/// This this property to true can increase performance, as Visible will change less often.
/// </summary>
/// <value>
/// <c>true</c> if approximate content bounds comparison is used; otherwise, <c>false</c>.
/// </value>
public bool UseApproximateContentBoundsComparison
{
get { return useApproximateContentBoundsComparison; }
set { useApproximateContentBoundsComparison = value; }
}
private double maxContentBoundsComparisonMistake = 0.02;
public double MaxContentBoundsComparisonMistake
{
get { return maxContentBoundsComparisonMistake; }
set { maxContentBoundsComparisonMistake = value; }
}
private DataRect prevContentBounds = DataRect.Empty;
protected virtual DataRect CoerceVisible(DataRect newVisible)
{
if (Plotter == null)
{
return newVisible;
}
bool isDefaultValue = newVisible == (DataRect)VisibleProperty.DefaultMetadata.DefaultValue;
if (isDefaultValue)
{
newVisible = DataRect.Empty;
}
if (isDefaultValue && IsFittedToView)
{
// determining content bounds
DataRect bounds = DataRect.Empty;
foreach (var item in contentBoundsHosts)
{
IPlotterElement plotterElement = item as IPlotterElement;
if (plotterElement == null)
continue;
if (plotterElement.Plotter == null)
continue;
var plotter = (Plotter2D)plotterElement.Plotter;
var visual = plotter.VisualBindings[plotterElement];
if (visual.Visibility == Visibility.Visible)
{
DataRect contentBounds = Viewport2D.GetContentBounds(item);
if (contentBounds.Width.IsNaN() || contentBounds.Height.IsNaN())
continue;
bounds.UnionFinite(contentBounds);
}
}
if (useApproximateContentBoundsComparison)
{
var intersection = prevContentBounds;
intersection.Intersect(bounds);
double currSquare = bounds.GetSquare();
double prevSquare = prevContentBounds.GetSquare();
double intersectionSquare = intersection.GetSquare();
double squareTopLimit = 1 + maxContentBoundsComparisonMistake;
double squareBottomLimit = 1 - maxContentBoundsComparisonMistake;
if (intersectionSquare != 0)
{
double currRatio = currSquare / intersectionSquare;
double prevRatio = prevSquare / intersectionSquare;
if (squareBottomLimit < currRatio &&
currRatio < squareTopLimit &&
squareBottomLimit < prevRatio &&
prevRatio < squareTopLimit)
{
bounds = prevContentBounds;
}
}
}
prevContentBounds = bounds;
UnitedContentBounds = bounds;
// applying fit-to-view restrictions
bounds = fitToViewRestrictions.Apply(Visible, bounds, this);
// enlarging
if (!bounds.IsEmpty)
{
bounds = CoordinateUtilities.RectZoom(bounds, bounds.GetCenter(), clipToBoundsEnlargeFactor);
}
else
{
bounds = (DataRect)VisibleProperty.DefaultMetadata.DefaultValue;
}
newVisible.Union(bounds);
}
if (newVisible.IsEmpty)
{
newVisible = (DataRect)VisibleProperty.DefaultMetadata.DefaultValue;
}
else if (newVisible.Width == 0 || newVisible.Height == 0)
{
DataRect defRect = (DataRect)VisibleProperty.DefaultMetadata.DefaultValue;
Size size = newVisible.Size;
Point loc = newVisible.Location;
if (newVisible.Width == 0)
{
size.Width = defRect.Width;
loc.X -= size.Width / 2;
}
if (newVisible.Height == 0)
{
size.Height = defRect.Height;
loc.Y -= size.Height / 2;
}
newVisible = new DataRect(loc, size);
}
// apply domain restriction
newVisible = domainRestriction.Apply(Visible, newVisible, this);
// apply other restrictions
newVisible = restrictions.Apply(Visible, newVisible, this);
// applying transform's data domain restriction
if (!transform.DataTransform.DataDomain.IsEmpty)
{
var newDataRect = newVisible.ViewportToData(transform);
newDataRect = DataRect.Intersect(newDataRect, transform.DataTransform.DataDomain);
newVisible = newDataRect.DataToViewport(transform);
}
if (newVisible.IsEmpty) newVisible = new Rect(0, 0, 1, 1);
return newVisible;
}
private static object OnCoerceVisible(DependencyObject d, object newValue)
{
Viewport2D viewport = (Viewport2D)d;
DataRect newRect = viewport.CoerceVisible((DataRect)newValue);
if (newRect.Width == 0 || newRect.Height == 0)
{
// doesn't apply rects with zero square
return DependencyProperty.UnsetValue;
}
else
{
return newRect;
}
}
#endregion
#region Domain
private readonly DomainRestriction domainRestriction = new DomainRestriction { Domain = Rect.Empty };
/// <summary>
/// Gets or sets the domain - rectangle in viewport coordinates that limits maximal size of <see cref="Visible"/> rectangle.
/// </summary>
/// <value>The domain.</value>
public DataRect Domain
{
get { return domainRestriction.Domain; }
set
{
if (domainRestriction.Domain != value)
{
domainRestriction.Domain = value;
DomainChanged.Raise(this);
CoerceValue(VisibleProperty);
}
}
}
/// <summary>
/// Occurs when <see cref="Domain"/> property changes.
/// </summary>
public event EventHandler DomainChanged;
#endregion
private double clipToBoundsEnlargeFactor = 1.10;
/// <summary>
/// Gets or sets the viewport enlarge factor.
/// </summary>
/// <remarks>
/// Default value is 1.10.
/// </remarks>
/// <value>The clip to bounds factor.</value>
public double ClipToBoundsEnlargeFactor
{
get { return clipToBoundsEnlargeFactor; }
set
{
if (clipToBoundsEnlargeFactor != value)
{
clipToBoundsEnlargeFactor = value;
UpdateVisible();
}
}
}
private void UpdateTransform()
{
transform = transform.WithRects(Visible, Output);
}
private CoordinateTransform transform = CoordinateTransform.CreateDefault();
/// <summary>
/// Gets or sets the coordinate transform of Viewport.
/// </summary>
/// <value>The transform.</value>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[NotNull]
public virtual CoordinateTransform Transform
{
get { return transform; }
set
{
value.VerifyNotNull();
if (value != transform)
{
var oldTransform = transform;
transform = value;
RaisePropertyChangedEvent("Transform", oldTransform, transform);
}
}
}
/// <summary>
/// Occurs when viewport property changes.
/// </summary>
public event EventHandler<ExtendedPropertyChangedEventArgs> PropertyChanged;
private void RaisePropertyChangedEvent(string propertyName, object oldValue, object newValue)
{
if (PropertyChanged != null)
{
RaisePropertyChanged(new ExtendedPropertyChangedEventArgs { PropertyName = propertyName, OldValue = oldValue, NewValue = newValue });
}
}
private void RaisePropertyChangedEvent(string propertyName)
{
if (PropertyChanged != null)
{
RaisePropertyChanged(new ExtendedPropertyChangedEventArgs { PropertyName = propertyName });
}
}
private void RaisePropertyChangedEvent(DependencyPropertyChangedEventArgs e)
{
if (PropertyChanged != null)
{
RaisePropertyChanged(ExtendedPropertyChangedEventArgs.FromDependencyPropertyChanged(e));
}
}
//private DispatcherOperation pendingRaisePropertyChangedOperation;
//private bool inRaisePropertyChanged = false;
protected virtual void RaisePropertyChanged(ExtendedPropertyChangedEventArgs args)
{
//if (inRaisePropertyChanged)
//{
// if (pendingRaisePropertyChangedOperation != null)
// pendingRaisePropertyChangedOperation.Abort();
// pendingRaisePropertyChangedOperation = Dispatcher.BeginInvoke(() => RaisePropertyChanged(args), DispatcherPriority.Normal);
// return;
//}
//pendingRaisePropertyChangedOperation = null;
//inRaisePropertyChanged = true;
PropertyChanged.Raise(this, args);
//inRaisePropertyChanged = false;
}
private void OnPlotterChildrenChanged(object sender, NotifyCollectionChangedEventArgs e)
{
UpdateContentBoundsHosts();
}
#region Panning state
private Viewport2DPanningState panningState = Viewport2DPanningState.NotPanning;
public Viewport2DPanningState PanningState
{
get { return panningState; }
set
{
var prevState = panningState;
panningState = value;
OnPanningStateChanged(prevState, panningState);
}
}
private void OnPanningStateChanged(Viewport2DPanningState prevState, Viewport2DPanningState currState)
{
PanningStateChanged.Raise(this, prevState, currState);
if (currState == Viewport2DPanningState.Panning)
BeginPanning.Raise(this);
else if (currState == Viewport2DPanningState.NotPanning)
EndPanning.Raise(this);
}
internal event EventHandler<ValueChangedEventArgs<Viewport2DPanningState>> PanningStateChanged;
public event EventHandler BeginPanning;
public event EventHandler EndPanning;
#endregion // end of Panning state
}
}