/Main/src/DynamicDataDisplay/Viewport2D.cs
C# | 697 lines | 499 code | 109 blank | 89 comment | 75 complexity | 198a54162d01ed2081a86ea6e63548a7 MD5 | raw file
Possible License(s): CC-BY-SA-3.0
1using System; 2using System.Collections.ObjectModel; 3using System.Collections.Specialized; 4using System.ComponentModel; 5using System.Diagnostics; 6using System.Diagnostics.CodeAnalysis; 7using System.Windows; 8using System.Windows.Data; 9using Microsoft.Research.DynamicDataDisplay.Common.Auxiliary; 10using Microsoft.Research.DynamicDataDisplay.ViewportConstraints; 11using Microsoft.Research.DynamicDataDisplay.Common; 12using System.Windows.Threading; 13 14namespace Microsoft.Research.DynamicDataDisplay 15{ 16 /// <summary> 17 /// Viewport2D provides virtual coordinates. 18 /// </summary> 19 public partial class Viewport2D : DependencyObject 20 { 21 private DispatcherOperation updateVisibleOperation = null; 22 private int updateVisibleCounter = 0; 23 private readonly RingDictionary<DataRect> prevVisibles = new RingDictionary<DataRect>(2); 24 private bool fromContentBounds = false; 25 private readonly DispatcherPriority invocationPriority = DispatcherPriority.Send; 26 27 public const string VisiblePropertyName = "Visible"; 28 29 public bool FromContentBounds 30 { 31 get { return fromContentBounds; } 32 set { fromContentBounds = value; } 33 } 34 35 private readonly Plotter2D plotter; 36 internal Plotter2D Plotter2D 37 { 38 get { return plotter; } 39 } 40 41 private readonly FrameworkElement hostElement; 42 internal FrameworkElement HostElement 43 { 44 get { return hostElement; } 45 } 46 47 protected internal Viewport2D(FrameworkElement host, Plotter2D plotter) 48 { 49 hostElement = host; 50 host.ClipToBounds = true; 51 host.SizeChanged += OnHostElementSizeChanged; 52 53 this.plotter = plotter; 54 plotter.Children.CollectionChanged += OnPlotterChildrenChanged; 55 56 constraints = new ConstraintCollection(this); 57 constraints.Add(new MinimalSizeConstraint()); 58 constraints.CollectionChanged += constraints_CollectionChanged; 59 60 fitToViewConstraints = new ConstraintCollection(this); 61 fitToViewConstraints.CollectionChanged += fitToViewConstraints_CollectionChanged; 62 63 readonlyContentBoundsHosts = new ReadOnlyObservableCollection<DependencyObject>(contentBoundsHosts); 64 65 UpdateVisible(); 66 UpdateTransform(); 67 } 68 69 private void OnHostElementSizeChanged(object sender, SizeChangedEventArgs e) 70 { 71 SetValue(OutputPropertyKey, new Rect(e.NewSize)); 72 CoerceValue(VisibleProperty); 73 } 74 75 private void fitToViewConstraints_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) 76 { 77 if (IsFittedToView) 78 { 79 CoerceValue(VisibleProperty); 80 } 81 } 82 83 private void constraints_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) 84 { 85 CoerceValue(VisibleProperty); 86 } 87 88 private static void OnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) 89 { 90 Viewport2D viewport = (Viewport2D)d; 91 viewport.UpdateTransform(); 92 viewport.RaisePropertyChangedEvent(e); 93 } 94 95 public BindingExpressionBase SetBinding(DependencyProperty property, BindingBase binding) 96 { 97 return BindingOperations.SetBinding(this, property, binding); 98 } 99 100 /// <summary> 101 /// Forces viewport to go to fit to view mode - clears locally set value of <see cref="Visible"/> property 102 /// and sets it during the coercion process to a value of united content bounds of all charts inside of <see cref="Plotter"/>. 103 /// </summary> 104 public void FitToView() 105 { 106 if (!IsFittedToView) 107 { 108 ClearValue(VisibleProperty); 109 CoerceValue(VisibleProperty); 110 FittedToView.Raise(this); 111 } 112 } 113 114 public event EventHandler FittedToView; 115 116 /// <summary> 117 /// Gets a value indicating whether Viewport is fitted to view. 118 /// </summary> 119 /// <value> 120 /// <c>true</c> if Viewport is fitted to view; otherwise, <c>false</c>. 121 /// </value> 122 [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] 123 public bool IsFittedToView 124 { 125 get { return ReadLocalValue(VisibleProperty) == DependencyProperty.UnsetValue; } 126 } 127 128 internal void UpdateVisible() 129 { 130 if (updateVisibleCounter == 0) 131 { 132 UpdateVisibleBody(); 133 } 134 else if (updateVisibleOperation == null) 135 { 136 updateVisibleOperation = Dispatcher.BeginInvoke(() => UpdateVisibleBody(), invocationPriority); 137 return; 138 } 139 else if (updateVisibleOperation.Status == DispatcherOperationStatus.Pending) 140 { 141 updateVisibleOperation.Abort(); 142 updateVisibleOperation = Dispatcher.BeginInvoke(() => UpdateVisibleBody(), invocationPriority); 143 } 144 } 145 146 private void UpdateVisibleBody() 147 { 148 if (updateVisibleCounter > 0) 149 return; 150 151 updateVisibleCounter++; 152 if (IsFittedToView) 153 { 154 CoerceValue(VisibleProperty); 155 } 156 updateVisibleOperation = null; 157 updateVisibleCounter--; 158 } 159 160 [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] 161 [EditorBrowsable(EditorBrowsableState.Never)] 162 public Plotter2D Plotter 163 { 164 get { return plotter; } 165 } 166 167 private readonly ConstraintCollection constraints; 168 /// <summary> 169 /// Gets the collection of <see cref="ViewportConstraint"/>s that are applied each time <see cref="Visible"/> is updated. 170 /// </summary> 171 /// <value>The constraints.</value> 172 [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)] 173 public ConstraintCollection Constraints 174 { 175 get { return constraints; } 176 } 177 178 179 private readonly ConstraintCollection fitToViewConstraints; 180 181 /// <summary> 182 /// Gets the collection of <see cref="ViewportConstraint"/>s that are applied only when Viewport is fitted to view. 183 /// </summary> 184 /// <value>The fit to view constraints.</value> 185 [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)] 186 public ConstraintCollection FitToViewConstraints 187 { 188 get { return fitToViewConstraints; } 189 } 190 191 #region Output property 192 193 /// <summary> 194 /// Gets the rectangle in screen coordinates that is output. 195 /// </summary> 196 /// <value>The output.</value> 197 public Rect Output 198 { 199 get { return (Rect)GetValue(OutputProperty); } 200 } 201 202 [SuppressMessage("Microsoft.Security", "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes")] 203 private static readonly DependencyPropertyKey OutputPropertyKey = DependencyProperty.RegisterReadOnly( 204 "Output", 205 typeof(Rect), 206 typeof(Viewport2D), 207 new FrameworkPropertyMetadata(new Rect(0, 0, 1, 1), OnPropertyChanged)); 208 209 /// <summary> 210 /// Identifies the <see cref="Output"/> dependency property. 211 /// </summary> 212 public static readonly DependencyProperty OutputProperty = OutputPropertyKey.DependencyProperty; 213 214 #endregion 215 216 #region UnitedContentBounds property 217 218 /// <summary> 219 /// Gets the united content bounds of all the charts. 220 /// </summary> 221 /// <value>The content bounds.</value> 222 public DataRect UnitedContentBounds 223 { 224 get { return (DataRect)GetValue(UnitedContentBoundsProperty); } 225 internal set { SetValue(UnitedContentBoundsProperty, value); } 226 } 227 228 /// <summary> 229 /// Identifies the <see cref="UnitedContentBounds"/> dependency property. 230 /// </summary> 231 public static readonly DependencyProperty UnitedContentBoundsProperty = DependencyProperty.Register( 232 "UnitedContentBounds", 233 typeof(DataRect), 234 typeof(Viewport2D), 235 new FrameworkPropertyMetadata(DataRect.Empty, OnUnitedContentBoundsChanged)); 236 237 private static void OnUnitedContentBoundsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) 238 { 239 Viewport2D owner = (Viewport2D)d; 240 owner.ContentBoundsChanged.Raise(owner); 241 } 242 243 public event EventHandler ContentBoundsChanged; 244 245 #endregion 246 247 #region Visible property 248 249 /// <summary> 250 /// Gets or sets the visible rectangle. 251 /// </summary> 252 /// <value>The visible.</value> 253 public DataRect Visible 254 { 255 get { return (DataRect)GetValue(VisibleProperty); } 256 set { SetValue(VisibleProperty, value); } 257 } 258 259 /// <summary> 260 /// Identifies the <see cref="Visible"/> dependency property. 261 /// </summary> 262 public static readonly DependencyProperty VisibleProperty = 263 DependencyProperty.Register("Visible", typeof(DataRect), typeof(Viewport2D), 264 new FrameworkPropertyMetadata( 265 new DataRect(0, 0, 1, 1), 266 OnPropertyChanged, 267 OnCoerceVisible), 268 ValidateVisibleCallback); 269 270 private static bool ValidateVisibleCallback(object value) 271 { 272 DataRect rect = (DataRect)value; 273 274 return !rect.IsNaN(); 275 } 276 277 private void UpdateContentBoundsHosts() 278 { 279 contentBoundsHosts.Clear(); 280 foreach (var item in plotter.Children) 281 { 282 DependencyObject dependencyObject = item as DependencyObject; 283 if (dependencyObject != null) 284 { 285 bool hasNonEmptyBounds = !Viewport2D.GetContentBounds(dependencyObject).IsEmpty; 286 if (hasNonEmptyBounds && Viewport2D.GetIsContentBoundsHost(dependencyObject)) 287 { 288 contentBoundsHosts.Add(dependencyObject); 289 } 290 } 291 } 292 293 bool prevFromContentBounds = FromContentBounds; 294 FromContentBounds = true; 295 UpdateVisible(); 296 FromContentBounds = prevFromContentBounds; 297 } 298 299 private readonly ObservableCollection<DependencyObject> contentBoundsHosts = new ObservableCollection<DependencyObject>(); 300 private readonly ReadOnlyObservableCollection<DependencyObject> readonlyContentBoundsHosts; 301 /// <summary> 302 /// Gets the collection of all charts that can has its own content bounds. 303 /// </summary> 304 /// <value>The content bounds hosts.</value> 305 [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] 306 public ReadOnlyObservableCollection<DependencyObject> ContentBoundsHosts 307 { 308 get { return readonlyContentBoundsHosts; } 309 } 310 311 private bool useApproximateContentBoundsComparison = true; 312 /// <summary> 313 /// Gets or sets a value indicating whether to use approximate content bounds comparison. 314 /// This this property to true can increase performance, as Visible will change less often. 315 /// </summary> 316 /// <value> 317 /// <c>true</c> if approximate content bounds comparison is used; otherwise, <c>false</c>. 318 /// </value> 319 public bool UseApproximateContentBoundsComparison 320 { 321 get { return useApproximateContentBoundsComparison; } 322 set { useApproximateContentBoundsComparison = value; } 323 } 324 325 private double maxContentBoundsComparisonMistake = 0.02; 326 public double MaxContentBoundsComparisonMistake 327 { 328 get { return maxContentBoundsComparisonMistake; } 329 set { maxContentBoundsComparisonMistake = value; } 330 } 331 332 private DataRect prevContentBounds = DataRect.Empty; 333 protected virtual DataRect CoerceVisible(DataRect newVisible) 334 { 335 if (Plotter == null) 336 { 337 return newVisible; 338 } 339 340 bool isDefaultValue = newVisible == (DataRect)VisibleProperty.DefaultMetadata.DefaultValue; 341 if (isDefaultValue) 342 { 343 newVisible = DataRect.Empty; 344 } 345 346 if (isDefaultValue && IsFittedToView) 347 { 348 // determining content bounds 349 DataRect bounds = DataRect.Empty; 350 351 foreach (var item in contentBoundsHosts) 352 { 353 IPlotterElement plotterElement = item as IPlotterElement; 354 if (plotterElement == null) 355 continue; 356 if (plotterElement.Plotter == null) 357 continue; 358 359 DataRect contentBounds = Viewport2D.GetContentBounds(item); 360 if (contentBounds.Width.IsNaN() || contentBounds.Height.IsNaN()) 361 continue; 362 363 bounds.UnionFinite(contentBounds); 364 } 365 366 if (useApproximateContentBoundsComparison) 367 { 368 var intersection = prevContentBounds; 369 intersection.Intersect(bounds); 370 371 double currSquare = bounds.GetSquare(); 372 double prevSquare = prevContentBounds.GetSquare(); 373 double intersectionSquare = intersection.GetSquare(); 374 375 double squareTopLimit = 1 + maxContentBoundsComparisonMistake; 376 double squareBottomLimit = 1 - maxContentBoundsComparisonMistake; 377 378 if (intersectionSquare != 0) 379 { 380 double currRatio = currSquare / intersectionSquare; 381 double prevRatio = prevSquare / intersectionSquare; 382 383 if (squareBottomLimit < currRatio && 384 currRatio < squareTopLimit && 385 squareBottomLimit < prevRatio && 386 prevRatio < squareTopLimit) 387 { 388 bounds = prevContentBounds; 389 } 390 } 391 } 392 393 prevContentBounds = bounds; 394 UnitedContentBounds = bounds; 395 396 // applying fit-to-view constraints 397 bounds = fitToViewConstraints.Apply(Visible, bounds, this); 398 399 // enlarging 400 if (!bounds.IsEmpty) 401 { 402 bounds = CoordinateUtilities.RectZoom(bounds, bounds.GetCenter(), clipToBoundsEnlargeFactor); 403 } 404 else 405 { 406 bounds = (DataRect)VisibleProperty.DefaultMetadata.DefaultValue; 407 } 408 newVisible.Union(bounds); 409 } 410 411 if (newVisible.IsEmpty) 412 { 413 newVisible = (DataRect)VisibleProperty.DefaultMetadata.DefaultValue; 414 } 415 else if (newVisible.Width == 0 || newVisible.Height == 0 || newVisible.IsEmptyX || newVisible.IsEmptyY) 416 { 417 DataRect defRect = (DataRect)VisibleProperty.DefaultMetadata.DefaultValue; 418 Size size = newVisible.Size; 419 Point location = newVisible.Location; 420 421 if (newVisible.Width == 0 || newVisible.IsEmptyX) 422 { 423 size.Width = defRect.Width; 424 location.X -= size.Width / 2; 425 } 426 if (newVisible.Height == 0 || newVisible.IsEmptyY) 427 { 428 size.Height = defRect.Height; 429 location.Y -= size.Height / 2; 430 } 431 432 newVisible = new DataRect(location, size); 433 } 434 435 // apply domain constraint 436 newVisible = domainConstraint.Apply(Visible, newVisible, this); 437 438 // apply other restrictions 439 newVisible = constraints.Apply(Visible, newVisible, this); 440 441 // applying transform's data domain constraint 442 if (!transform.DataTransform.DataDomain.IsEmpty) 443 { 444 var newDataRect = newVisible.ViewportToData(transform); 445 newDataRect = DataRect.Intersect(newDataRect, transform.DataTransform.DataDomain); 446 newVisible = newDataRect.DataToViewport(transform); 447 } 448 449 if (newVisible.IsEmpty) 450 newVisible = new Rect(0, 0, 1, 1); 451 452 return newVisible; 453 } 454 455 private static object OnCoerceVisible(DependencyObject d, object newValue) 456 { 457 Viewport2D viewport = (Viewport2D)d; 458 DataRect newVisible = (DataRect)newValue; 459 460 DataRect newRect = viewport.CoerceVisible(newVisible); 461 462 if (viewport.FromContentBounds && viewport.prevVisibles.ContainsValue(newRect)) 463 return DependencyProperty.UnsetValue; 464 else 465 viewport.prevVisibles.AddValue(newRect); 466 467 if (newRect.Width == 0 || newRect.Height == 0) 468 { 469 // doesn't apply rects with zero square 470 return DependencyProperty.UnsetValue; 471 } 472 else 473 { 474 return newRect; 475 } 476 } 477 478 #endregion 479 480 #region Domain 481 482 private readonly DomainConstraint domainConstraint = new DomainConstraint { Domain = Rect.Empty }; 483 484 /// <summary> 485 /// Gets or sets the domain - rectangle in viewport coordinates that limits maximal size of <see cref="Visible"/> rectangle. 486 /// </summary> 487 /// <value>The domain.</value> 488 public DataRect Domain 489 { 490 get { return (DataRect)GetValue(DomainProperty); } 491 set { SetValue(DomainProperty, value); } 492 } 493 494 public static readonly DependencyProperty DomainProperty = DependencyProperty.Register( 495 "Domain", 496 typeof(DataRect), 497 typeof(Viewport2D), 498 new FrameworkPropertyMetadata(DataRect.Empty, OnDomainReplaced)); 499 500 private static void OnDomainReplaced(DependencyObject d, DependencyPropertyChangedEventArgs e) 501 { 502 Viewport2D owner = (Viewport2D)d; 503 owner.OnDomainChanged(); 504 } 505 506 private void OnDomainChanged() 507 { 508 domainConstraint.Domain = Domain; 509 DomainChanged.Raise(this); 510 CoerceValue(VisibleProperty); 511 } 512 513 /// <summary> 514 /// Occurs when <see cref="Domain"/> property changes. 515 /// </summary> 516 public event EventHandler DomainChanged; 517 518 #endregion 519 520 private double clipToBoundsEnlargeFactor = 1.10; 521 /// <summary> 522 /// Gets or sets the viewport enlarge factor. 523 /// </summary> 524 /// <remarks> 525 /// Default value is 1.10. 526 /// </remarks> 527 /// <value>The clip to bounds factor.</value> 528 public double ClipToBoundsEnlargeFactor 529 { 530 get { return clipToBoundsEnlargeFactor; } 531 set 532 { 533 if (clipToBoundsEnlargeFactor != value) 534 { 535 clipToBoundsEnlargeFactor = value; 536 UpdateVisible(); 537 } 538 } 539 } 540 541 private void UpdateTransform() 542 { 543 transform = transform.WithRects(Visible, Output); 544 } 545 546 private CoordinateTransform transform = CoordinateTransform.CreateDefault(); 547 /// <summary> 548 /// Gets or sets the coordinate transform of Viewport. 549 /// </summary> 550 /// <value>The transform.</value> 551 [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] 552 [NotNull] 553 public virtual CoordinateTransform Transform 554 { 555 get { return transform; } 556 set 557 { 558 value.VerifyNotNull(); 559 560 if (value != transform) 561 { 562 var oldTransform = transform; 563 564 transform = value; 565 566 RaisePropertyChangedEvent("Transform", oldTransform, transform); 567 } 568 } 569 } 570 571 /// <summary> 572 /// Occurs when viewport property changes. 573 /// </summary> 574 public event EventHandler<ExtendedPropertyChangedEventArgs> PropertyChanged; 575 576 private void RaisePropertyChangedEvent(string propertyName, object oldValue, object newValue) 577 { 578 if (PropertyChanged != null) 579 { 580 RaisePropertyChanged(new ExtendedPropertyChangedEventArgs { PropertyName = propertyName, OldValue = oldValue, NewValue = newValue }); 581 } 582 } 583 584 private void RaisePropertyChangedEvent(string propertyName) 585 { 586 if (PropertyChanged != null) 587 { 588 RaisePropertyChanged(new ExtendedPropertyChangedEventArgs { PropertyName = propertyName }); 589 } 590 } 591 592 /// <summary> 593 /// Gets or sets the type of viewport change. 594 /// </summary> 595 /// <value>The type of the change.</value> 596 public ChangeType ChangeType { get; internal set; } 597 598 /// <summary> 599 /// Sets the type of viewport change. 600 /// </summary> 601 /// <param name="changeType">Type of the change.</param> 602 public void SetChangeType(ChangeType changeType = ChangeType.None) 603 { 604 this.ChangeType = changeType; 605 } 606 607 int propertyChangedCounter = 0; 608 private DispatcherOperation notifyOperation = null; 609 private void RaisePropertyChangedEvent(DependencyPropertyChangedEventArgs e) 610 { 611 if (notifyOperation == null) 612 { 613 ChangeType changeType = ChangeType; 614 notifyOperation = Dispatcher.BeginInvoke(() => 615 { 616 RaisePropertyChangedEventBody(e, changeType); 617 }, invocationPriority); 618 return; 619 } 620 else if (notifyOperation.Status == DispatcherOperationStatus.Pending) 621 { 622 notifyOperation.Abort(); 623 ChangeType changeType = ChangeType; 624 notifyOperation = Dispatcher.BeginInvoke(() => 625 { 626 RaisePropertyChangedEventBody(e, changeType); 627 }, invocationPriority); 628 } 629 } 630 631 private void RaisePropertyChangedEventBody(DependencyPropertyChangedEventArgs e, ChangeType changeType) 632 { 633 if (propertyChangedCounter > 0) 634 return; 635 636 propertyChangedCounter++; 637 RaisePropertyChanged(ExtendedPropertyChangedEventArgs.FromDependencyPropertyChanged(e), changeType); 638 propertyChangedCounter--; 639 640 notifyOperation = null; 641 } 642 643 protected virtual void RaisePropertyChanged(ExtendedPropertyChangedEventArgs args, ChangeType changeType = ChangeType.None) 644 { 645 args.ChangeType = changeType; 646 PropertyChanged.Raise(this, args); 647 } 648 649 private void OnPlotterChildrenChanged(object sender, NotifyCollectionChangedEventArgs e) 650 { 651 UpdateContentBoundsHosts(); 652 } 653 654 #region Panning state 655 656 private Viewport2DPanningState panningState = Viewport2DPanningState.NotPanning; 657 public Viewport2DPanningState PanningState 658 { 659 get { return panningState; } 660 set 661 { 662 var prevState = panningState; 663 664 panningState = value; 665 666 OnPanningStateChanged(prevState, panningState); 667 } 668 } 669 670 private void OnPanningStateChanged(Viewport2DPanningState prevState, Viewport2DPanningState currState) 671 { 672 PanningStateChanged.Raise(this, prevState, currState); 673 if (currState == Viewport2DPanningState.Panning) 674 BeginPanning.Raise(this); 675 else if (currState == Viewport2DPanningState.NotPanning) 676 EndPanning.Raise(this); 677 } 678 679 internal event EventHandler<ValueChangedEventArgs<Viewport2DPanningState>> PanningStateChanged; 680 681 public event EventHandler BeginPanning; 682 public event EventHandler EndPanning; 683 684 #endregion // end of Panning state 685 } 686 687 public enum ChangeType 688 { 689 None = 0, 690 Pan, 691 PanX, 692 PanY, 693 Zoom, 694 ZoomX, 695 ZoomY 696 } 697}