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.ViewportConstraints;
using Microsoft.Research.DynamicDataDisplay.Common;
using System.Windows.Threading;
namespace Microsoft.Research.DynamicDataDisplay
{
///
/// Viewport2D provides virtual coordinates.
///
public partial class Viewport2D : DependencyObject
{
private DispatcherOperation updateVisibleOperation = null;
private int updateVisibleCounter = 0;
private readonly RingDictionary prevVisibles = new RingDictionary(2);
private bool fromContentBounds = false;
private readonly DispatcherPriority invocationPriority = DispatcherPriority.Send;
public const string VisiblePropertyName = "Visible";
public bool FromContentBounds
{
get { return fromContentBounds; }
set { fromContentBounds = value; }
}
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;
constraints = new ConstraintCollection(this);
constraints.Add(new MinimalSizeConstraint());
constraints.CollectionChanged += constraints_CollectionChanged;
fitToViewConstraints = new ConstraintCollection(this);
fitToViewConstraints.CollectionChanged += fitToViewConstraints_CollectionChanged;
readonlyContentBoundsHosts = new ReadOnlyObservableCollection(contentBoundsHosts);
UpdateVisible();
UpdateTransform();
}
private void OnHostElementSizeChanged(object sender, SizeChangedEventArgs e)
{
SetValue(OutputPropertyKey, new Rect(e.NewSize));
CoerceValue(VisibleProperty);
}
private void fitToViewConstraints_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (IsFittedToView)
{
CoerceValue(VisibleProperty);
}
}
private void constraints_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);
}
///
/// Forces viewport to go to fit to view mode - clears locally set value of property
/// and sets it during the coercion process to a value of united content bounds of all charts inside of .
///
public void FitToView()
{
if (!IsFittedToView)
{
ClearValue(VisibleProperty);
CoerceValue(VisibleProperty);
FittedToView.Raise(this);
}
}
public event EventHandler FittedToView;
///
/// Gets a value indicating whether Viewport is fitted to view.
///
///
/// true if Viewport is fitted to view; otherwise, false.
///
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public bool IsFittedToView
{
get { return ReadLocalValue(VisibleProperty) == DependencyProperty.UnsetValue; }
}
internal void UpdateVisible()
{
if (updateVisibleCounter == 0)
{
UpdateVisibleBody();
}
else if (updateVisibleOperation == null)
{
updateVisibleOperation = Dispatcher.BeginInvoke(() => UpdateVisibleBody(), invocationPriority);
return;
}
else if (updateVisibleOperation.Status == DispatcherOperationStatus.Pending)
{
updateVisibleOperation.Abort();
updateVisibleOperation = Dispatcher.BeginInvoke(() => UpdateVisibleBody(), invocationPriority);
}
}
private void UpdateVisibleBody()
{
if (updateVisibleCounter > 0)
return;
updateVisibleCounter++;
if (IsFittedToView)
{
CoerceValue(VisibleProperty);
}
updateVisibleOperation = null;
updateVisibleCounter--;
}
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[EditorBrowsable(EditorBrowsableState.Never)]
public Plotter2D Plotter
{
get { return plotter; }
}
private readonly ConstraintCollection constraints;
///
/// Gets the collection of s that are applied each time is updated.
///
/// The constraints.
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
public ConstraintCollection Constraints
{
get { return constraints; }
}
private readonly ConstraintCollection fitToViewConstraints;
///
/// Gets the collection of s that are applied only when Viewport is fitted to view.
///
/// The fit to view constraints.
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
public ConstraintCollection FitToViewConstraints
{
get { return fitToViewConstraints; }
}
#region Output property
///
/// Gets the rectangle in screen coordinates that is output.
///
/// The output.
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));
///
/// Identifies the dependency property.
///
public static readonly DependencyProperty OutputProperty = OutputPropertyKey.DependencyProperty;
#endregion
#region UnitedContentBounds property
///
/// Gets the united content bounds of all the charts.
///
/// The content bounds.
public DataRect UnitedContentBounds
{
get { return (DataRect)GetValue(UnitedContentBoundsProperty); }
internal set { SetValue(UnitedContentBoundsProperty, value); }
}
///
/// Identifies the dependency property.
///
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
///
/// Gets or sets the visible rectangle.
///
/// The visible.
public DataRect Visible
{
get { return (DataRect)GetValue(VisibleProperty); }
set { SetValue(VisibleProperty, value); }
}
///
/// Identifies the dependency property.
///
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);
}
}
}
bool prevFromContentBounds = FromContentBounds;
FromContentBounds = true;
UpdateVisible();
FromContentBounds = prevFromContentBounds;
}
private readonly ObservableCollection contentBoundsHosts = new ObservableCollection();
private readonly ReadOnlyObservableCollection readonlyContentBoundsHosts;
///
/// Gets the collection of all charts that can has its own content bounds.
///
/// The content bounds hosts.
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public ReadOnlyObservableCollection ContentBoundsHosts
{
get { return readonlyContentBoundsHosts; }
}
private bool useApproximateContentBoundsComparison = true;
///
/// 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.
///
///
/// true if approximate content bounds comparison is used; otherwise, false.
///
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;
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 constraints
bounds = fitToViewConstraints.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 || newVisible.IsEmptyX || newVisible.IsEmptyY)
{
DataRect defRect = (DataRect)VisibleProperty.DefaultMetadata.DefaultValue;
Size size = newVisible.Size;
Point location = newVisible.Location;
if (newVisible.Width == 0 || newVisible.IsEmptyX)
{
size.Width = defRect.Width;
location.X -= size.Width / 2;
}
if (newVisible.Height == 0 || newVisible.IsEmptyY)
{
size.Height = defRect.Height;
location.Y -= size.Height / 2;
}
newVisible = new DataRect(location, size);
}
// apply domain constraint
newVisible = domainConstraint.Apply(Visible, newVisible, this);
// apply other restrictions
newVisible = constraints.Apply(Visible, newVisible, this);
// applying transform's data domain constraint
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 newVisible = (DataRect)newValue;
DataRect newRect = viewport.CoerceVisible(newVisible);
if (viewport.FromContentBounds && viewport.prevVisibles.ContainsValue(newRect))
return DependencyProperty.UnsetValue;
else
viewport.prevVisibles.AddValue(newRect);
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 DomainConstraint domainConstraint = new DomainConstraint { Domain = Rect.Empty };
///
/// Gets or sets the domain - rectangle in viewport coordinates that limits maximal size of rectangle.
///
/// The domain.
public DataRect Domain
{
get { return (DataRect)GetValue(DomainProperty); }
set { SetValue(DomainProperty, value); }
}
public static readonly DependencyProperty DomainProperty = DependencyProperty.Register(
"Domain",
typeof(DataRect),
typeof(Viewport2D),
new FrameworkPropertyMetadata(DataRect.Empty, OnDomainReplaced));
private static void OnDomainReplaced(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
Viewport2D owner = (Viewport2D)d;
owner.OnDomainChanged();
}
private void OnDomainChanged()
{
domainConstraint.Domain = Domain;
DomainChanged.Raise(this);
CoerceValue(VisibleProperty);
}
///
/// Occurs when property changes.
///
public event EventHandler DomainChanged;
#endregion
private double clipToBoundsEnlargeFactor = 1.10;
///
/// Gets or sets the viewport enlarge factor.
///
///
/// Default value is 1.10.
///
/// The clip to bounds factor.
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();
///
/// Gets or sets the coordinate transform of Viewport.
///
/// The transform.
[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);
}
}
}
///
/// Occurs when viewport property changes.
///
public event EventHandler 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 });
}
}
///
/// Gets or sets the type of viewport change.
///
/// The type of the change.
public ChangeType ChangeType { get; internal set; }
///
/// Sets the type of viewport change.
///
/// Type of the change.
public void SetChangeType(ChangeType changeType = ChangeType.None)
{
this.ChangeType = changeType;
}
int propertyChangedCounter = 0;
private DispatcherOperation notifyOperation = null;
private void RaisePropertyChangedEvent(DependencyPropertyChangedEventArgs e)
{
if (notifyOperation == null)
{
ChangeType changeType = ChangeType;
notifyOperation = Dispatcher.BeginInvoke(() =>
{
RaisePropertyChangedEventBody(e, changeType);
}, invocationPriority);
return;
}
else if (notifyOperation.Status == DispatcherOperationStatus.Pending)
{
notifyOperation.Abort();
ChangeType changeType = ChangeType;
notifyOperation = Dispatcher.BeginInvoke(() =>
{
RaisePropertyChangedEventBody(e, changeType);
}, invocationPriority);
}
}
private void RaisePropertyChangedEventBody(DependencyPropertyChangedEventArgs e, ChangeType changeType)
{
if (propertyChangedCounter > 0)
return;
propertyChangedCounter++;
RaisePropertyChanged(ExtendedPropertyChangedEventArgs.FromDependencyPropertyChanged(e), changeType);
propertyChangedCounter--;
notifyOperation = null;
}
protected virtual void RaisePropertyChanged(ExtendedPropertyChangedEventArgs args, ChangeType changeType = ChangeType.None)
{
args.ChangeType = changeType;
PropertyChanged.Raise(this, args);
}
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> PanningStateChanged;
public event EventHandler BeginPanning;
public event EventHandler EndPanning;
#endregion // end of Panning state
}
public enum ChangeType
{
None = 0,
Pan,
PanX,
PanY,
Zoom,
ZoomX,
ZoomY
}
}