Initial Commit
This commit is contained in:
87
Charts/Isolines/AdditionalLinesRenderer.cs
Normal file
87
Charts/Isolines/AdditionalLinesRenderer.cs
Normal file
@@ -0,0 +1,87 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Windows.Media;
|
||||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Microsoft.Research.DynamicDataDisplay.Charts.Isolines
|
||||
{
|
||||
public class AdditionalLinesRenderer : IsolineRenderer
|
||||
{
|
||||
protected override void CreateUIRepresentation()
|
||||
{
|
||||
InvalidateVisual();
|
||||
}
|
||||
|
||||
protected override void OnPlotterAttached()
|
||||
{
|
||||
base.OnPlotterAttached();
|
||||
|
||||
FrameworkElement parent = (FrameworkElement)Parent;
|
||||
var renderer = (FrameworkElement)parent.FindName("PART_IsolineRenderer");
|
||||
|
||||
Binding contentBoundsBinding = new Binding { Path = new PropertyPath("(0)", Viewport2D.ContentBoundsProperty), Source = renderer };
|
||||
SetBinding(Viewport2D.ContentBoundsProperty, contentBoundsBinding);
|
||||
SetBinding(ViewportPanel.ViewportBoundsProperty, contentBoundsBinding);
|
||||
|
||||
Plotter2D.Viewport.EndPanning += Viewport_EndPanning;
|
||||
Plotter2D.Viewport.PropertyChanged += Viewport_PropertyChanged;
|
||||
}
|
||||
|
||||
void Viewport_PropertyChanged(object sender, ExtendedPropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == "Visible")
|
||||
{
|
||||
if (Plotter2D.Viewport.PanningState == Viewport2DPanningState.NotPanning)
|
||||
InvalidateVisual();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnPlotterDetaching()
|
||||
{
|
||||
Plotter2D.Viewport.EndPanning -= Viewport_EndPanning;
|
||||
Plotter2D.Viewport.PropertyChanged -= Viewport_PropertyChanged;
|
||||
|
||||
base.OnPlotterDetaching();
|
||||
}
|
||||
|
||||
private void Viewport_EndPanning(object sender, EventArgs e)
|
||||
{
|
||||
InvalidateVisual();
|
||||
}
|
||||
|
||||
protected override void OnRender(DrawingContext drawingContext)
|
||||
{
|
||||
if (Plotter2D == null) return;
|
||||
if (DataSource == null) return;
|
||||
|
||||
var collection = (IsolineCollection)Parent.GetValue(IsolineCollectionProperty);
|
||||
if (collection == null) return;
|
||||
|
||||
var bounds = ViewportPanel.GetViewportBounds(this);
|
||||
if (bounds.IsEmpty) return;
|
||||
|
||||
var dc = drawingContext;
|
||||
var strokeThickness = StrokeThickness;
|
||||
|
||||
var transform = Plotter2D.Transform.WithRects(bounds, new Rect(RenderSize));
|
||||
|
||||
//dc.DrawRectangle(null, new Pen(Brushes.Green, 2), new Rect(RenderSize));
|
||||
|
||||
var additionalLevels = GetAdditionalLevels(collection);
|
||||
IsolineBuilder.DataSource = DataSource;
|
||||
var additionalIsolineCollections = additionalLevels.Select(level =>
|
||||
{
|
||||
return IsolineBuilder.BuildIsoline(level);
|
||||
});
|
||||
|
||||
foreach (var additionalCollection in additionalIsolineCollections)
|
||||
{
|
||||
RenderIsolineCollection(dc, strokeThickness, additionalCollection, transform);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
399
Charts/Isolines/CellInfo.cs
Normal file
399
Charts/Isolines/CellInfo.cs
Normal file
@@ -0,0 +1,399 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Diagnostics;
|
||||
using Microsoft.Research.DynamicDataDisplay.Common.Auxiliary;
|
||||
using System.Windows;
|
||||
|
||||
namespace Microsoft.Research.DynamicDataDisplay.Charts.Isolines
|
||||
{
|
||||
/// <summary>
|
||||
/// Isoline's grid cell
|
||||
/// </summary>
|
||||
internal interface ICell
|
||||
{
|
||||
Vector LeftTop { get; }
|
||||
Vector LeftBottom { get; }
|
||||
Vector RightTop { get; }
|
||||
Vector RightBottom { get; }
|
||||
}
|
||||
|
||||
internal sealed class IrregularCell : ICell
|
||||
{
|
||||
public IrregularCell(Vector leftBottom, Vector rightBottom, Vector rightTop, Vector leftTop)
|
||||
{
|
||||
this.leftBottom = leftBottom;
|
||||
this.rightBottom = rightBottom;
|
||||
this.rightTop = rightTop;
|
||||
this.leftTop = leftTop;
|
||||
}
|
||||
|
||||
public IrregularCell(Point lb, Point rb, Point rt, Point lt)
|
||||
{
|
||||
leftTop = lt.ToVector();
|
||||
leftBottom = lb.ToVector();
|
||||
rightTop = rt.ToVector();
|
||||
rightBottom = rb.ToVector();
|
||||
}
|
||||
|
||||
#region ICell Members
|
||||
|
||||
private readonly Vector leftTop;
|
||||
public Vector LeftTop
|
||||
{
|
||||
get { return leftTop; }
|
||||
}
|
||||
|
||||
private readonly Vector leftBottom;
|
||||
public Vector LeftBottom
|
||||
{
|
||||
get { return leftBottom; }
|
||||
}
|
||||
|
||||
private readonly Vector rightTop;
|
||||
public Vector RightTop
|
||||
{
|
||||
get { return rightTop; }
|
||||
}
|
||||
|
||||
private readonly Vector rightBottom;
|
||||
public Vector RightBottom
|
||||
{
|
||||
get { return rightBottom; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Sides
|
||||
public Vector LeftSide
|
||||
{
|
||||
get { return (leftBottom + leftTop) / 2; }
|
||||
}
|
||||
|
||||
public Vector RightSide
|
||||
{
|
||||
get { return (rightBottom + rightTop) / 2; }
|
||||
}
|
||||
public Vector TopSide
|
||||
{
|
||||
get { return (leftTop + rightTop) / 2; }
|
||||
}
|
||||
public Vector BottomSide
|
||||
{
|
||||
get { return (leftBottom + rightBottom) / 2; }
|
||||
}
|
||||
#endregion
|
||||
|
||||
public Point Center
|
||||
{
|
||||
get { return ((LeftSide + RightSide) / 2).ToPoint(); }
|
||||
}
|
||||
|
||||
public IrregularCell GetSubRect(SubCell sub)
|
||||
{
|
||||
switch (sub)
|
||||
{
|
||||
case SubCell.LeftBottom:
|
||||
return new IrregularCell(LeftBottom, BottomSide, Center.ToVector(), LeftSide);
|
||||
case SubCell.LeftTop:
|
||||
return new IrregularCell(LeftSide, Center.ToVector(), TopSide, LeftTop);
|
||||
case SubCell.RightBottom:
|
||||
return new IrregularCell(BottomSide, RightBottom, RightSide, Center.ToVector());
|
||||
case SubCell.RightTop:
|
||||
default:
|
||||
return new IrregularCell(Center.ToVector(), RightSide, RightTop, TopSide);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal enum SubCell
|
||||
{
|
||||
LeftBottom = 0,
|
||||
LeftTop = 1,
|
||||
RightBottom = 2,
|
||||
RightTop = 3
|
||||
}
|
||||
|
||||
internal class ValuesInCell
|
||||
{
|
||||
double min = Double.MaxValue, max = Double.MinValue;
|
||||
|
||||
/// <summary>Initializes values in four corners of cell</summary>
|
||||
/// <param name="leftBottom"></param>
|
||||
/// <param name="rightBottom"></param>
|
||||
/// <param name="rightTop"></param>
|
||||
/// <param name="leftTop"></param>
|
||||
/// <remarks>Some or all values can be NaN. That means that value is not specified (misssing)</remarks>
|
||||
public ValuesInCell(double leftBottom, double rightBottom, double rightTop, double leftTop)
|
||||
{
|
||||
this.leftTop = leftTop;
|
||||
this.leftBottom = leftBottom;
|
||||
this.rightTop = rightTop;
|
||||
this.rightBottom = rightBottom;
|
||||
|
||||
// Find max and min values (with respect to possible NaN values)
|
||||
if (!Double.IsNaN(leftTop))
|
||||
{
|
||||
if (min > leftTop)
|
||||
min = leftTop;
|
||||
if (max < leftTop)
|
||||
max = leftTop;
|
||||
}
|
||||
|
||||
if (!Double.IsNaN(leftBottom))
|
||||
{
|
||||
if (min > leftBottom)
|
||||
min = leftBottom;
|
||||
if (max < leftBottom)
|
||||
max = leftBottom;
|
||||
}
|
||||
|
||||
if (!Double.IsNaN(rightTop))
|
||||
{
|
||||
if (min > rightTop)
|
||||
min = rightTop;
|
||||
if (max < rightTop)
|
||||
max = rightTop;
|
||||
}
|
||||
|
||||
if (!Double.IsNaN(rightBottom))
|
||||
{
|
||||
if (min > rightBottom)
|
||||
min = rightBottom;
|
||||
if (max < rightBottom)
|
||||
max = rightBottom;
|
||||
}
|
||||
|
||||
left = (leftTop + leftBottom) / 2;
|
||||
bottom = (leftBottom + rightBottom) / 2;
|
||||
right = (rightTop + rightBottom) / 2;
|
||||
top = (rightTop + leftTop) / 2;
|
||||
}
|
||||
|
||||
public ValuesInCell(double leftBottom, double rightBottom, double rightTop, double leftTop, double missingValue)
|
||||
{
|
||||
DebugVerify.IsNotNaN(leftBottom);
|
||||
DebugVerify.IsNotNaN(rightBottom);
|
||||
DebugVerify.IsNotNaN(rightTop);
|
||||
DebugVerify.IsNotNaN(leftTop);
|
||||
|
||||
// Copy values and find min and max with respect to possible missing values
|
||||
if (leftTop != missingValue)
|
||||
{
|
||||
this.leftTop = leftTop;
|
||||
if (min > leftTop)
|
||||
min = leftTop;
|
||||
if (max < leftTop)
|
||||
max = leftTop;
|
||||
}
|
||||
else
|
||||
this.leftTop = Double.NaN;
|
||||
|
||||
if (leftBottom != missingValue)
|
||||
{
|
||||
this.leftBottom = leftBottom;
|
||||
if (min > leftBottom)
|
||||
min = leftBottom;
|
||||
if (max < leftBottom)
|
||||
max = leftBottom;
|
||||
}
|
||||
else
|
||||
this.leftBottom = Double.NaN;
|
||||
|
||||
if (rightTop != missingValue)
|
||||
{
|
||||
this.rightTop = rightTop;
|
||||
if (min > rightTop)
|
||||
min = rightTop;
|
||||
if (max < rightTop)
|
||||
max = rightTop;
|
||||
}
|
||||
else
|
||||
this.rightTop = Double.NaN;
|
||||
|
||||
if (rightBottom != missingValue)
|
||||
{
|
||||
this.rightBottom = rightBottom;
|
||||
if (min > rightBottom)
|
||||
min = rightBottom;
|
||||
if (max < rightBottom)
|
||||
max = rightBottom;
|
||||
}
|
||||
else
|
||||
this.rightBottom = Double.NaN;
|
||||
|
||||
left = (this.leftTop + this.leftBottom) / 2;
|
||||
bottom = (this.leftBottom + this.rightBottom) / 2;
|
||||
right = (this.rightTop + this.rightBottom) / 2;
|
||||
top = (this.rightTop + this.leftTop) / 2;
|
||||
|
||||
|
||||
/*
|
||||
if (leftTop != missingValue && )
|
||||
{
|
||||
if (leftBottom != missingValue)
|
||||
left = (leftTop + leftBottom) / 2;
|
||||
else
|
||||
left = Double.NaN;
|
||||
|
||||
if (rightTop != missingValue)
|
||||
top = (leftTop + rightTop) / 2;
|
||||
else
|
||||
top = Double.NaN;
|
||||
}
|
||||
|
||||
if (rightBottom != missingValue)
|
||||
{
|
||||
if (leftBottom != missingValue)
|
||||
bottom = (leftBottom + rightBottom) / 2;
|
||||
else
|
||||
bottom = Double.NaN;
|
||||
|
||||
if (rightTop != missingValue)
|
||||
right = (rightTop + rightBottom) / 2;
|
||||
else
|
||||
right = Double.NaN;
|
||||
}*/
|
||||
}
|
||||
|
||||
|
||||
/*internal bool ValueBelongTo(double value)
|
||||
{
|
||||
IEnumerable<double> values = new double[] { leftTop, leftBottom, rightTop, rightBottom };
|
||||
|
||||
return !(values.All(v => v > value) || values.All(v => v < value));
|
||||
}*/
|
||||
|
||||
internal bool ValueBelongTo(double value)
|
||||
{
|
||||
return (min <= value && value <= max);
|
||||
}
|
||||
|
||||
#region Edges
|
||||
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
|
||||
private readonly double leftTop;
|
||||
public double LeftTop { get { return leftTop; } }
|
||||
|
||||
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
|
||||
private readonly double leftBottom;
|
||||
public double LeftBottom { get { return leftBottom; } }
|
||||
|
||||
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
|
||||
private readonly double rightTop;
|
||||
public double RightTop
|
||||
{
|
||||
get { return rightTop; }
|
||||
}
|
||||
|
||||
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
|
||||
private readonly double rightBottom;
|
||||
public double RightBottom
|
||||
{
|
||||
get { return rightBottom; }
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Sides & center
|
||||
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
|
||||
private readonly double left;
|
||||
public double Left
|
||||
{
|
||||
get { return left; }
|
||||
}
|
||||
|
||||
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
|
||||
private readonly double right;
|
||||
public double Right
|
||||
{
|
||||
get { return right; }
|
||||
}
|
||||
|
||||
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
|
||||
private readonly double top;
|
||||
public double Top
|
||||
{
|
||||
get { return top; }
|
||||
}
|
||||
|
||||
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
|
||||
private readonly double bottom;
|
||||
public double Bottom
|
||||
{
|
||||
get { return bottom; }
|
||||
}
|
||||
|
||||
public double Center
|
||||
{
|
||||
get { return (Left + Right) * 0.5; }
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region SubCells
|
||||
public ValuesInCell LeftTopCell
|
||||
{
|
||||
get { return new ValuesInCell(Left, Center, Top, LeftTop); }
|
||||
}
|
||||
|
||||
public ValuesInCell RightTopCell
|
||||
{
|
||||
get { return new ValuesInCell(Center, Right, RightTop, Top); }
|
||||
}
|
||||
|
||||
public ValuesInCell RightBottomCell
|
||||
{
|
||||
get { return new ValuesInCell(Bottom, RightBottom, Right, Center); }
|
||||
}
|
||||
|
||||
public ValuesInCell LeftBottomCell
|
||||
{
|
||||
get { return new ValuesInCell(LeftBottom, Bottom, Center, Left); }
|
||||
}
|
||||
|
||||
public ValuesInCell GetSubCell(SubCell subCell)
|
||||
{
|
||||
switch (subCell)
|
||||
{
|
||||
case SubCell.LeftBottom:
|
||||
return LeftBottomCell;
|
||||
case SubCell.LeftTop:
|
||||
return LeftTopCell;
|
||||
case SubCell.RightBottom:
|
||||
return RightBottomCell;
|
||||
case SubCell.RightTop:
|
||||
default:
|
||||
return RightTopCell;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Returns bitmask of comparison of values at cell corners with reference value.
|
||||
/// Corresponding bit is set to one if value at cell corner is greater than reference value.
|
||||
/// a------b
|
||||
/// | Cell |
|
||||
/// d------c
|
||||
/// </summary>
|
||||
/// <param name="a">Value at corner (see figure)</param>
|
||||
/// <param name="b">Value at corner (see figure)</param>
|
||||
/// <param name="c">Value at corner (see figure)</param>
|
||||
/// <param name="d">Value at corner (see figure)</param>
|
||||
/// <param name="value">Reference value</param>
|
||||
/// <returns>Bitmask</returns>
|
||||
public CellBitmask GetCellValue(double value)
|
||||
{
|
||||
CellBitmask n = CellBitmask.None;
|
||||
if (!Double.IsNaN(leftTop) && leftTop > value)
|
||||
n |= CellBitmask.LeftTop;
|
||||
if (!Double.IsNaN(leftBottom) && leftBottom > value)
|
||||
n |= CellBitmask.LeftBottom;
|
||||
if (!Double.IsNaN(rightBottom) && rightBottom > value)
|
||||
n |= CellBitmask.RightBottom;
|
||||
if (!Double.IsNaN(rightTop) && rightTop > value)
|
||||
n |= CellBitmask.RightTop;
|
||||
|
||||
return n;
|
||||
}
|
||||
}
|
||||
}
|
||||
67
Charts/Isolines/Enums.cs
Normal file
67
Charts/Isolines/Enums.cs
Normal file
@@ -0,0 +1,67 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Microsoft.Research.DynamicDataDisplay.Charts.Isolines
|
||||
{
|
||||
/// <summary>
|
||||
/// Edge identifier - indicates which side of cell isoline crosses.
|
||||
/// </summary>
|
||||
internal enum Edge
|
||||
{
|
||||
// todo check if everything is ok with None.
|
||||
None = 0,
|
||||
/// <summary>
|
||||
/// Isoline crosses left boundary of cell (bit 0)
|
||||
/// </summary>
|
||||
Left = 1,
|
||||
/// <summary>
|
||||
/// Isoline crosses top boundary of cell (bit 1)
|
||||
/// </summary>
|
||||
Top = 2,
|
||||
/// <summary>
|
||||
/// Isoline crosses right boundary of cell (bit 2)
|
||||
/// </summary>
|
||||
Right = 4,
|
||||
/// <summary>
|
||||
/// Isoline crosses bottom boundary of cell (bit 3)
|
||||
/// </summary>
|
||||
Bottom = 8
|
||||
}
|
||||
|
||||
[Flags]
|
||||
internal enum CellBitmask
|
||||
{
|
||||
None = 0,
|
||||
LeftTop = 1,
|
||||
LeftBottom = 8,
|
||||
RightBottom = 4,
|
||||
RightTop = 2
|
||||
}
|
||||
|
||||
internal static class IsolineExtensions
|
||||
{
|
||||
internal static bool IsDiagonal(this CellBitmask bitmask)
|
||||
{
|
||||
return bitmask == (CellBitmask.RightBottom | CellBitmask.LeftTop) ||
|
||||
bitmask == (CellBitmask.LeftBottom | CellBitmask.RightTop);
|
||||
}
|
||||
|
||||
internal static bool IsAppropriate(this SubCell sub, Edge edge)
|
||||
{
|
||||
switch (sub)
|
||||
{
|
||||
case SubCell.LeftBottom:
|
||||
return edge == Edge.Left || edge == Edge.Bottom;
|
||||
case SubCell.LeftTop:
|
||||
return edge == Edge.Left || edge == Edge.Top;
|
||||
case SubCell.RightBottom:
|
||||
return edge == Edge.Right || edge == Edge.Bottom;
|
||||
case SubCell.RightTop:
|
||||
default:
|
||||
return edge == Edge.Right || edge == Edge.Top;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
19
Charts/Isolines/FastIsolineDisplay.xaml
Normal file
19
Charts/Isolines/FastIsolineDisplay.xaml
Normal file
@@ -0,0 +1,19 @@
|
||||
<isolines:IsolineGraphBase x:Class="Microsoft.Research.DynamicDataDisplay.Charts.Isolines.FastIsolineDisplay"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:isolines="clr-namespace:Microsoft.Research.DynamicDataDisplay.Charts.Isolines"
|
||||
xmlns:local="clr-namespace:Microsoft.Research.DynamicDataDisplay.Charts"
|
||||
xmlns:common="clr-namespace:Microsoft.Research.DynamicDataDisplay"
|
||||
IsHitTestVisible="False"
|
||||
>
|
||||
<isolines:IsolineGraphBase.Template>
|
||||
<ControlTemplate TargetType="{x:Type isolines:IsolineGraphBase}">
|
||||
<Canvas IsHitTestVisible="False">
|
||||
<local:ViewportHostPanel Name="PART_ViewportPanel">
|
||||
<isolines:FastIsolineRenderer Name="PART_IsolineRenderer"/>
|
||||
<isolines:AdditionalLinesRenderer Name="PART_AdditionalLinesRenderer"/>
|
||||
</local:ViewportHostPanel>
|
||||
</Canvas>
|
||||
</ControlTemplate>
|
||||
</isolines:IsolineGraphBase.Template>
|
||||
</isolines:IsolineGraphBase>
|
||||
59
Charts/Isolines/FastIsolineDisplay.xaml.cs
Normal file
59
Charts/Isolines/FastIsolineDisplay.xaml.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Navigation;
|
||||
using System.Windows.Shapes;
|
||||
using Microsoft.Research.DynamicDataDisplay.Charts.Shapes;
|
||||
|
||||
namespace Microsoft.Research.DynamicDataDisplay.Charts.Isolines
|
||||
{
|
||||
public partial class FastIsolineDisplay : IsolineGraphBase
|
||||
{
|
||||
public FastIsolineDisplay()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
protected override Panel HostPanel
|
||||
{
|
||||
get
|
||||
{
|
||||
return Plotter2D.CentralGrid;
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnApplyTemplate()
|
||||
{
|
||||
base.OnApplyTemplate();
|
||||
|
||||
var isolineRenderer = (FastIsolineRenderer)Template.FindName("PART_IsolineRenderer", this);
|
||||
//Binding contentBoundsBinding = new Binding { Path = new PropertyPath("(0)", Viewport2D.ContentBoundsProperty), Source = isolineRenderer };
|
||||
//SetBinding(Viewport2D.ContentBoundsProperty, contentBoundsBinding);
|
||||
|
||||
if (isolineRenderer != null)
|
||||
{
|
||||
isolineRenderer.AddHandler(Viewport2D.ContentBoundsChangedEvent, new RoutedEventHandler(OnRendererContentBoundsChanged));
|
||||
UpdateContentBounds(isolineRenderer);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnRendererContentBoundsChanged(object sender, RoutedEventArgs e)
|
||||
{
|
||||
UpdateContentBounds((DependencyObject)sender);
|
||||
}
|
||||
|
||||
private void UpdateContentBounds(DependencyObject source)
|
||||
{
|
||||
var contentBounds = Viewport2D.GetContentBounds(source);
|
||||
Viewport2D.SetContentBounds(this, contentBounds);
|
||||
}
|
||||
}
|
||||
}
|
||||
200
Charts/Isolines/FastIsolineRenderer.cs
Normal file
200
Charts/Isolines/FastIsolineRenderer.cs
Normal file
@@ -0,0 +1,200 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Windows.Media;
|
||||
using System.Windows;
|
||||
using System.Diagnostics;
|
||||
using Microsoft.Research.DynamicDataDisplay.Common.Auxiliary;
|
||||
using Microsoft.Research.DynamicDataDisplay.Charts.Shapes;
|
||||
using System.Windows.Threading;
|
||||
using System.Globalization;
|
||||
using Microsoft.Research.DynamicDataDisplay.Charts.NewLine;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace Microsoft.Research.DynamicDataDisplay.Charts.Isolines
|
||||
{
|
||||
public class FastIsolineRenderer : IsolineRenderer
|
||||
{
|
||||
private List<IsolineCollection> additionalLines = new List<IsolineCollection>();
|
||||
private const int subDivisionNum = 10;
|
||||
|
||||
protected override void CreateUIRepresentation()
|
||||
{
|
||||
InvalidateVisual();
|
||||
}
|
||||
|
||||
protected override void OnPlotterAttached()
|
||||
{
|
||||
base.OnPlotterAttached();
|
||||
|
||||
FrameworkElement parent = (FrameworkElement)Parent;
|
||||
Binding collectionBinding = new Binding("IsolineCollection") { Source = this };
|
||||
parent.SetBinding(IsolineCollectionProperty, collectionBinding);
|
||||
}
|
||||
|
||||
protected override void OnRender(DrawingContext drawingContext)
|
||||
{
|
||||
if (Plotter2D == null) return;
|
||||
if (Collection == null) return;
|
||||
if (DataSource == null) return;
|
||||
if (Collection.Lines.Count == 0)
|
||||
{
|
||||
IsolineBuilder.DataSource = DataSource;
|
||||
IsolineBuilder.MissingValue = MissingValue;
|
||||
Collection = IsolineBuilder.BuildIsoline();
|
||||
}
|
||||
|
||||
IsolineCollection = Collection;
|
||||
|
||||
var dc = drawingContext;
|
||||
var strokeThickness = StrokeThickness;
|
||||
var collection = Collection;
|
||||
|
||||
var bounds = DataRect.Empty;
|
||||
// determining content bounds
|
||||
foreach (LevelLine line in collection)
|
||||
{
|
||||
foreach (Point point in line.AllPoints)
|
||||
{
|
||||
bounds.Union(point);
|
||||
}
|
||||
}
|
||||
|
||||
Viewport2D.SetContentBounds(this, bounds);
|
||||
ViewportPanel.SetViewportBounds(this, bounds);
|
||||
|
||||
if (bounds.IsEmpty) return;
|
||||
|
||||
// custom transform with output set to renderSize of this control
|
||||
var transform = Plotter2D.Transform.WithRects(bounds, new Rect(RenderSize));
|
||||
|
||||
// actual drawing of isolines
|
||||
RenderIsolineCollection(dc, strokeThickness, collection, transform);
|
||||
|
||||
//var additionalLevels = GetAdditionalIsolines(collection);
|
||||
|
||||
//var additionalIsolineCollections = additionalLevels.Select(level => IsolineBuilder.BuildIsoline(level));
|
||||
|
||||
//foreach (var additionalCollection in additionalIsolineCollections)
|
||||
//{
|
||||
// RenderIsolineCollection(dc, strokeThickness, additionalCollection, transform);
|
||||
//}
|
||||
|
||||
RenderLabels(dc, collection);
|
||||
|
||||
// foreach (var additionalCollection in additionalIsolineCollections)
|
||||
// {
|
||||
// RenderLabels(dc, additionalCollection);
|
||||
// }
|
||||
}
|
||||
|
||||
private IEnumerable<double> GetAdditionalIsolines(IsolineCollection collection)
|
||||
{
|
||||
var dataSource = DataSource;
|
||||
var visibleMinMax = dataSource.GetMinMax(Plotter2D.Visible);
|
||||
var visibleMinMaxRatio = (collection.Max - collection.Min) / visibleMinMax.GetLength();
|
||||
|
||||
var log = Math.Log10(visibleMinMaxRatio);
|
||||
if (log > 0.9)
|
||||
{
|
||||
var upperLog = Math.Ceiling(log);
|
||||
var divisionsNum = Math.Pow(10, upperLog);
|
||||
var delta = (collection.Max - collection.Min) / divisionsNum;
|
||||
|
||||
var start = Math.Ceiling(visibleMinMax.Min / delta) * delta;
|
||||
|
||||
var x = start;
|
||||
while (x < visibleMinMax.Max)
|
||||
{
|
||||
yield return x;
|
||||
x += delta;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RenderLabels(DrawingContext dc, IsolineCollection collection)
|
||||
{
|
||||
if (Plotter2D == null) return;
|
||||
if (collection == null) return;
|
||||
if (!DrawLabels) return;
|
||||
|
||||
var viewportBounds = ViewportPanel.GetViewportBounds(this);
|
||||
if (viewportBounds.IsEmpty)
|
||||
return;
|
||||
|
||||
var strokeThickness = StrokeThickness;
|
||||
var visible = Plotter2D.Visible;
|
||||
var output = Plotter2D.Viewport.Output;
|
||||
|
||||
var transform = Plotter2D.Transform.WithRects(viewportBounds, new Rect(RenderSize));
|
||||
var labelStringFormat = LabelStringFormat;
|
||||
|
||||
// drawing constants
|
||||
var labelRectangleFill = Brushes.White;
|
||||
|
||||
var biggerViewport = viewportBounds.ZoomOutFromCenter(1.1);
|
||||
|
||||
// getting and filtering annotations to draw only visible ones
|
||||
Annotater.WayBeforeText = Math.Sqrt(visible.Width * visible.Width + visible.Height * visible.Height) / 100 * WayBeforeTextMultiplier;
|
||||
var annotations = Annotater.Annotate(collection, visible)
|
||||
.Where(annotation =>
|
||||
{
|
||||
Point viewportPosition = annotation.Position.DataToViewport(transform);
|
||||
return biggerViewport.Contains(viewportPosition);
|
||||
});
|
||||
|
||||
// drawing annotations
|
||||
foreach (var annotation in annotations)
|
||||
{
|
||||
FormattedText text = CreateFormattedText(annotation.Value.ToString(LabelStringFormat));
|
||||
Point position = annotation.Position.DataToScreen(transform);
|
||||
|
||||
var labelTransform = CreateTransform(annotation, text, position);
|
||||
|
||||
// creating rectange stroke
|
||||
double colorRatio = (annotation.Value - collection.Min) / (collection.Max - collection.Min);
|
||||
colorRatio = MathHelper.Clamp(colorRatio);
|
||||
Color rectangleStrokeColor = Palette.GetColor(colorRatio);
|
||||
SolidColorBrush rectangleStroke = new SolidColorBrush(rectangleStrokeColor);
|
||||
Pen labelRectangleStrokePen = new Pen(rectangleStroke, 2);
|
||||
|
||||
dc.PushTransform(labelTransform);
|
||||
{
|
||||
var bounds = RectExtensions.FromCenterSize(position, new Size(text.Width, text.Height));
|
||||
bounds = bounds.ZoomOutFromCenter(1.3);
|
||||
dc.DrawRoundedRectangle(labelRectangleFill, labelRectangleStrokePen, bounds, 8, 8);
|
||||
|
||||
DrawTextInPosition(dc, text, position);
|
||||
}
|
||||
dc.Pop();
|
||||
}
|
||||
}
|
||||
|
||||
private static void DrawTextInPosition(DrawingContext dc, FormattedText text, Point position)
|
||||
{
|
||||
var textPosition = position;
|
||||
textPosition.Offset(-text.Width / 2, -text.Height / 2);
|
||||
dc.DrawText(text, textPosition);
|
||||
}
|
||||
|
||||
private static Transform CreateTransform(IsolineTextLabel isolineLabel, FormattedText text, Point position)
|
||||
{
|
||||
double angle = isolineLabel.Rotation;
|
||||
if (angle < 0)
|
||||
angle += 360;
|
||||
if (90 < angle && angle < 270)
|
||||
angle -= 180;
|
||||
|
||||
RotateTransform transform = new RotateTransform(angle, position.X, position.Y);
|
||||
return transform;
|
||||
}
|
||||
|
||||
private static FormattedText CreateFormattedText(string text)
|
||||
{
|
||||
FormattedText result = new FormattedText(text,
|
||||
CultureInfo.CurrentCulture, System.Windows.FlowDirection.LeftToRight, new Typeface("Arial"), 12, Brushes.Black,1.0);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
708
Charts/Isolines/IsolineBuilder.cs
Normal file
708
Charts/Isolines/IsolineBuilder.cs
Normal file
@@ -0,0 +1,708 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using Microsoft.Research.DynamicDataDisplay.Common.Auxiliary;
|
||||
using Microsoft.Research.DynamicDataDisplay.DataSources;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Microsoft.Research.DynamicDataDisplay.Charts.Isolines
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates geometric object for isolines of the input 2d scalar field.
|
||||
/// </summary>
|
||||
public sealed class IsolineBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// The density of isolines means the number of levels to draw.
|
||||
/// </summary>
|
||||
private int density = 12;
|
||||
|
||||
private bool[,] processed;
|
||||
|
||||
/// <summary>Number to be treated as missing value. NaN if no missing value is specified</summary>
|
||||
private double missingValue = Double.NaN;
|
||||
|
||||
static IsolineBuilder()
|
||||
{
|
||||
SetCellDictionaries();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="IsolineBuilder"/> class.
|
||||
/// </summary>
|
||||
public IsolineBuilder() { }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="IsolineBuilder"/> class for specified 2d scalar data source.
|
||||
/// </summary>
|
||||
/// <param name="dataSource">The data source with 2d scalar data.</param>
|
||||
public IsolineBuilder(IDataSource2D<double> dataSource)
|
||||
{
|
||||
DataSource = dataSource;
|
||||
}
|
||||
|
||||
public double MissingValue
|
||||
{
|
||||
get
|
||||
{
|
||||
return missingValue;
|
||||
}
|
||||
set
|
||||
{
|
||||
missingValue = value;
|
||||
}
|
||||
}
|
||||
|
||||
#region Private methods
|
||||
|
||||
private static Dictionary<int, Dictionary<int, Edge>> dictChooser = new Dictionary<int, Dictionary<int, Edge>>();
|
||||
private static void SetCellDictionaries()
|
||||
{
|
||||
var bottomDict = new Dictionary<int, Edge>();
|
||||
bottomDict.Add((int)CellBitmask.RightBottom, Edge.Right);
|
||||
bottomDict.Add(Edge.Left,
|
||||
CellBitmask.LeftTop,
|
||||
CellBitmask.LeftBottom | CellBitmask.RightBottom | CellBitmask.RightTop,
|
||||
CellBitmask.LeftTop | CellBitmask.RightBottom | CellBitmask.RightTop,
|
||||
CellBitmask.LeftBottom);
|
||||
bottomDict.Add(Edge.Right,
|
||||
CellBitmask.RightTop,
|
||||
CellBitmask.LeftBottom | CellBitmask.RightBottom | CellBitmask.LeftTop,
|
||||
CellBitmask.LeftBottom | CellBitmask.LeftTop | CellBitmask.RightTop);
|
||||
bottomDict.Add(Edge.Top,
|
||||
CellBitmask.RightBottom | CellBitmask.RightTop,
|
||||
CellBitmask.LeftBottom | CellBitmask.LeftTop);
|
||||
|
||||
var leftDict = new Dictionary<int, Edge>();
|
||||
leftDict.Add(Edge.Top,
|
||||
CellBitmask.LeftTop,
|
||||
CellBitmask.LeftBottom | CellBitmask.RightBottom | CellBitmask.RightTop);
|
||||
leftDict.Add(Edge.Right,
|
||||
CellBitmask.LeftTop | CellBitmask.RightTop,
|
||||
CellBitmask.LeftBottom | CellBitmask.RightBottom);
|
||||
leftDict.Add(Edge.Bottom,
|
||||
CellBitmask.RightBottom | CellBitmask.RightTop | CellBitmask.LeftTop,
|
||||
CellBitmask.LeftBottom);
|
||||
|
||||
var topDict = new Dictionary<int, Edge>();
|
||||
topDict.Add(Edge.Right,
|
||||
CellBitmask.RightTop,
|
||||
CellBitmask.LeftTop | CellBitmask.LeftBottom | CellBitmask.RightBottom);
|
||||
topDict.Add(Edge.Right,
|
||||
CellBitmask.RightBottom,
|
||||
CellBitmask.LeftTop | CellBitmask.LeftBottom | CellBitmask.RightTop);
|
||||
topDict.Add(Edge.Left,
|
||||
CellBitmask.RightBottom | CellBitmask.RightTop | CellBitmask.LeftTop,
|
||||
CellBitmask.LeftBottom,
|
||||
CellBitmask.LeftTop,
|
||||
CellBitmask.LeftBottom | CellBitmask.RightBottom | CellBitmask.RightTop);
|
||||
topDict.Add(Edge.Bottom,
|
||||
CellBitmask.RightBottom | CellBitmask.RightTop,
|
||||
CellBitmask.LeftTop | CellBitmask.LeftBottom);
|
||||
|
||||
var rightDict = new Dictionary<int, Edge>();
|
||||
rightDict.Add(Edge.Top,
|
||||
CellBitmask.RightTop,
|
||||
CellBitmask.LeftBottom | CellBitmask.RightBottom | CellBitmask.LeftTop);
|
||||
rightDict.Add(Edge.Left,
|
||||
CellBitmask.LeftTop | CellBitmask.RightTop,
|
||||
CellBitmask.LeftBottom | CellBitmask.RightBottom);
|
||||
rightDict.Add(Edge.Bottom,
|
||||
CellBitmask.RightBottom,
|
||||
CellBitmask.LeftTop | CellBitmask.LeftBottom | CellBitmask.RightTop);
|
||||
|
||||
dictChooser.Add((int)Edge.Left, leftDict);
|
||||
dictChooser.Add((int)Edge.Right, rightDict);
|
||||
dictChooser.Add((int)Edge.Bottom, bottomDict);
|
||||
dictChooser.Add((int)Edge.Top, topDict);
|
||||
}
|
||||
|
||||
private Edge GetOutEdge(Edge inEdge, ValuesInCell cv, IrregularCell rect, double value)
|
||||
{
|
||||
// value smaller than all values in corners or
|
||||
// value greater than all values in corners
|
||||
if (!cv.ValueBelongTo(value))
|
||||
{
|
||||
throw new IsolineGenerationException(Strings.Exceptions.IsolinesValueIsOutOfCell);
|
||||
}
|
||||
|
||||
CellBitmask cellVal = cv.GetCellValue(value);
|
||||
var dict = dictChooser[(int)inEdge];
|
||||
if (dict.ContainsKey((int)cellVal))
|
||||
{
|
||||
Edge result = dict[(int)cellVal];
|
||||
switch (result)
|
||||
{
|
||||
case Edge.Left:
|
||||
if (cv.LeftTop.IsNaN() || cv.LeftBottom.IsNaN())
|
||||
result = Edge.None;
|
||||
break;
|
||||
case Edge.Right:
|
||||
if (cv.RightTop.IsNaN() || cv.RightBottom.IsNaN())
|
||||
result = Edge.None;
|
||||
break;
|
||||
case Edge.Top:
|
||||
if (cv.RightTop.IsNaN() || cv.LeftTop.IsNaN())
|
||||
result = Edge.None;
|
||||
break;
|
||||
case Edge.Bottom:
|
||||
if (cv.LeftBottom.IsNaN() || cv.RightBottom.IsNaN())
|
||||
result = Edge.None;
|
||||
break;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
else if (cellVal.IsDiagonal())
|
||||
{
|
||||
return GetOutForOpposite(inEdge, cellVal, value, cv, rect);
|
||||
}
|
||||
|
||||
const double near_zero = 0.0001;
|
||||
const double near_one = 1 - near_zero;
|
||||
|
||||
double lt = cv.LeftTop;
|
||||
double rt = cv.RightTop;
|
||||
double rb = cv.RightBottom;
|
||||
double lb = cv.LeftBottom;
|
||||
|
||||
switch (inEdge)
|
||||
{
|
||||
case Edge.Left:
|
||||
if (value == lt)
|
||||
value = near_one * lt + near_zero * lb;
|
||||
else if (value == lb)
|
||||
value = near_one * lb + near_zero * lt;
|
||||
else
|
||||
return Edge.None;
|
||||
// Now this is possible because of missing value
|
||||
//throw new IsolineGenerationException(Strings.Exceptions.IsolinesUnsupportedCase);
|
||||
break;
|
||||
case Edge.Top:
|
||||
if (value == rt)
|
||||
value = near_one * rt + near_zero * lt;
|
||||
else if (value == lt)
|
||||
value = near_one * lt + near_zero * rt;
|
||||
else
|
||||
return Edge.None;
|
||||
// Now this is possibe because of missing value
|
||||
//throw new IsolineGenerationException(Strings.Exceptions.IsolinesUnsupportedCase);
|
||||
break;
|
||||
case Edge.Right:
|
||||
if (value == rb)
|
||||
value = near_one * rb + near_zero * rt;
|
||||
else if (value == rt)
|
||||
value = near_one * rt + near_zero * rb;
|
||||
else
|
||||
return Edge.None;
|
||||
// Now this is possibe because of missing value
|
||||
//throw new IsolineGenerationException(Strings.Exceptions.IsolinesUnsupportedCase);
|
||||
break;
|
||||
case Edge.Bottom:
|
||||
if (value == rb)
|
||||
value = near_one * rb + near_zero * lb;
|
||||
else if (value == lb)
|
||||
value = near_one * lb + near_zero * rb;
|
||||
else
|
||||
return Edge.None;
|
||||
// Now this is possibe because of missing value
|
||||
//throw new IsolineGenerationException(Strings.Exceptions.IsolinesUnsupportedCase);
|
||||
break;
|
||||
}
|
||||
|
||||
// Recursion?
|
||||
//return GetOutEdge(inEdge, cv, rect, value);
|
||||
|
||||
return Edge.None;
|
||||
}
|
||||
|
||||
private Edge GetOutForOpposite(Edge inEdge, CellBitmask cellVal, double value, ValuesInCell cellValues, IrregularCell rect)
|
||||
{
|
||||
Edge outEdge;
|
||||
|
||||
SubCell subCell = GetSubCell(inEdge, value, cellValues);
|
||||
|
||||
int iters = 1000; // max number of iterations
|
||||
do
|
||||
{
|
||||
ValuesInCell subValues = cellValues.GetSubCell(subCell);
|
||||
IrregularCell subRect = rect.GetSubRect(subCell);
|
||||
outEdge = GetOutEdge(inEdge, subValues, subRect, value);
|
||||
if (outEdge == Edge.None)
|
||||
return Edge.None;
|
||||
bool isAppropriate = subCell.IsAppropriate(outEdge);
|
||||
if (isAppropriate)
|
||||
{
|
||||
ValuesInCell sValues = subValues.GetSubCell(subCell);
|
||||
|
||||
Point point = GetPointXY(outEdge, value, subValues, subRect);
|
||||
segments.AddPoint(point);
|
||||
return outEdge;
|
||||
}
|
||||
else
|
||||
{
|
||||
subCell = GetAdjacentEdge(subCell, outEdge);
|
||||
}
|
||||
|
||||
byte e = (byte)outEdge;
|
||||
inEdge = (Edge)((e > 2) ? (e >> 2) : (e << 2));
|
||||
iters--;
|
||||
} while (iters >= 0);
|
||||
|
||||
throw new IsolineGenerationException(Strings.Exceptions.IsolinesDataIsUndetailized);
|
||||
}
|
||||
|
||||
private static SubCell GetAdjacentEdge(SubCell sub, Edge edge)
|
||||
{
|
||||
SubCell res = SubCell.LeftBottom;
|
||||
|
||||
switch (sub)
|
||||
{
|
||||
case SubCell.LeftBottom:
|
||||
res = edge == Edge.Top ? SubCell.LeftTop : SubCell.RightBottom;
|
||||
break;
|
||||
case SubCell.LeftTop:
|
||||
res = edge == Edge.Bottom ? SubCell.LeftBottom : SubCell.RightTop;
|
||||
break;
|
||||
case SubCell.RightBottom:
|
||||
res = edge == Edge.Top ? SubCell.RightTop : SubCell.LeftBottom;
|
||||
break;
|
||||
case SubCell.RightTop:
|
||||
default:
|
||||
res = edge == Edge.Bottom ? SubCell.RightBottom : SubCell.LeftTop;
|
||||
break;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
private static SubCell GetSubCell(Edge inEdge, double value, ValuesInCell vc)
|
||||
{
|
||||
double lb = vc.LeftBottom;
|
||||
double rb = vc.RightBottom;
|
||||
double rt = vc.RightTop;
|
||||
double lt = vc.LeftTop;
|
||||
|
||||
SubCell res = SubCell.LeftBottom;
|
||||
switch (inEdge)
|
||||
{
|
||||
case Edge.Left:
|
||||
res = (Math.Abs(value - lb) < Math.Abs(value - lt)) ? SubCell.LeftBottom : SubCell.LeftTop;
|
||||
break;
|
||||
case Edge.Top:
|
||||
res = (Math.Abs(value - lt) < Math.Abs(value - rt)) ? SubCell.LeftTop : SubCell.RightTop;
|
||||
break;
|
||||
case Edge.Right:
|
||||
res = (Math.Abs(value - rb) < Math.Abs(value - rt)) ? SubCell.RightBottom : SubCell.RightTop;
|
||||
break;
|
||||
case Edge.Bottom:
|
||||
default:
|
||||
res = (Math.Abs(value - lb) < Math.Abs(value - rb)) ? SubCell.LeftBottom : SubCell.RightBottom;
|
||||
break;
|
||||
}
|
||||
|
||||
ValuesInCell subValues = vc.GetSubCell(res);
|
||||
bool valueInside = subValues.ValueBelongTo(value);
|
||||
if (!valueInside)
|
||||
{
|
||||
throw new IsolineGenerationException(Strings.Exceptions.IsolinesDataIsUndetailized);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
private static Point GetPoint(double value, double a1, double a2, Vector v1, Vector v2)
|
||||
{
|
||||
double ratio = (value - a1) / (a2 - a1);
|
||||
|
||||
Verify.IsTrue(0 <= ratio && ratio <= 1);
|
||||
|
||||
Vector r = (1 - ratio) * v1 + ratio * v2;
|
||||
return new Point(r.X, r.Y);
|
||||
}
|
||||
|
||||
private Point GetPointXY(Edge edge, double value, ValuesInCell vc, IrregularCell rect)
|
||||
{
|
||||
double lt = vc.LeftTop;
|
||||
double lb = vc.LeftBottom;
|
||||
double rb = vc.RightBottom;
|
||||
double rt = vc.RightTop;
|
||||
|
||||
switch (edge)
|
||||
{
|
||||
case Edge.Left:
|
||||
return GetPoint(value, lb, lt, rect.LeftBottom, rect.LeftTop);
|
||||
case Edge.Top:
|
||||
return GetPoint(value, lt, rt, rect.LeftTop, rect.RightTop);
|
||||
case Edge.Right:
|
||||
return GetPoint(value, rb, rt, rect.RightBottom, rect.RightTop);
|
||||
case Edge.Bottom:
|
||||
return GetPoint(value, lb, rb, rect.LeftBottom, rect.RightBottom);
|
||||
default:
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
}
|
||||
|
||||
private bool BelongsToEdge(double value, double edgeValue1, double edgeValue2, bool onBoundary)
|
||||
{
|
||||
if (!Double.IsNaN(missingValue) && (edgeValue1 == missingValue || edgeValue2 == missingValue))
|
||||
return false;
|
||||
|
||||
if (onBoundary)
|
||||
{
|
||||
return (edgeValue1 <= value && value < edgeValue2) ||
|
||||
(edgeValue2 <= value && value < edgeValue1);
|
||||
}
|
||||
else
|
||||
{
|
||||
return (edgeValue1 < value && value < edgeValue2) ||
|
||||
(edgeValue2 < value && value < edgeValue1);
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsPassed(Edge edge, int i, int j, byte[,] edges)
|
||||
{
|
||||
switch (edge)
|
||||
{
|
||||
case Edge.Left:
|
||||
return (i == 0) || (edges[i, j] & (byte)edge) != 0;
|
||||
case Edge.Bottom:
|
||||
return (j == 0) || (edges[i, j] & (byte)edge) != 0;
|
||||
case Edge.Top:
|
||||
return (j == edges.GetLength(1) - 2) || (edges[i, j + 1] & (byte)Edge.Bottom) != 0;
|
||||
case Edge.Right:
|
||||
return (i == edges.GetLength(0) - 2) || (edges[i + 1, j] & (byte)Edge.Left) != 0;
|
||||
default:
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
}
|
||||
|
||||
private void MakeEdgePassed(Edge edge, int i, int j)
|
||||
{
|
||||
switch (edge)
|
||||
{
|
||||
case Edge.Left:
|
||||
case Edge.Bottom:
|
||||
edges[i, j] |= (byte)edge;
|
||||
break;
|
||||
case Edge.Top:
|
||||
edges[i, j + 1] |= (byte)Edge.Bottom;
|
||||
break;
|
||||
case Edge.Right:
|
||||
edges[i + 1, j] |= (byte)Edge.Left;
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
}
|
||||
|
||||
private Edge TrackLine(Edge inEdge, double value, ref int x, ref int y, out double newX, out double newY)
|
||||
{
|
||||
// Getting output edge
|
||||
ValuesInCell vc = (missingValue.IsNaN()) ?
|
||||
(new ValuesInCell(values[x, y],
|
||||
values[x + 1, y],
|
||||
values[x + 1, y + 1],
|
||||
values[x, y + 1])) :
|
||||
(new ValuesInCell(values[x, y],
|
||||
values[x + 1, y],
|
||||
values[x + 1, y + 1],
|
||||
values[x, y + 1],
|
||||
missingValue));
|
||||
|
||||
IrregularCell rect = new IrregularCell(
|
||||
grid[x, y],
|
||||
grid[x + 1, y],
|
||||
grid[x + 1, y + 1],
|
||||
grid[x, y + 1]);
|
||||
|
||||
Edge outEdge = GetOutEdge(inEdge, vc, rect, value);
|
||||
if (outEdge == Edge.None)
|
||||
{
|
||||
newX = newY = -1; // Impossible cell indices
|
||||
return Edge.None;
|
||||
}
|
||||
|
||||
// Drawing new segment
|
||||
Point point = GetPointXY(outEdge, value, vc, rect);
|
||||
newX = point.X;
|
||||
newY = point.Y;
|
||||
segments.AddPoint(point);
|
||||
processed[x, y] = true;
|
||||
|
||||
// Whether out-edge already was passed?
|
||||
if (IsPassed(outEdge, x, y, edges)) // line is closed
|
||||
{
|
||||
//MakeEdgePassed(outEdge, x, y); // boundaries should be marked as passed too
|
||||
return Edge.None;
|
||||
}
|
||||
|
||||
// Make this edge passed
|
||||
MakeEdgePassed(outEdge, x, y);
|
||||
|
||||
// Getting next cell's indices
|
||||
switch (outEdge)
|
||||
{
|
||||
case Edge.Left:
|
||||
x--;
|
||||
return Edge.Right;
|
||||
case Edge.Top:
|
||||
y++;
|
||||
return Edge.Bottom;
|
||||
case Edge.Right:
|
||||
x++;
|
||||
return Edge.Left;
|
||||
case Edge.Bottom:
|
||||
y--;
|
||||
return Edge.Top;
|
||||
default:
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
}
|
||||
|
||||
private void TrackLineNonRecursive(Edge inEdge, double value, int x, int y)
|
||||
{
|
||||
int s = x, t = y;
|
||||
|
||||
ValuesInCell vc = (missingValue.IsNaN()) ?
|
||||
(new ValuesInCell(values[x, y],
|
||||
values[x + 1, y],
|
||||
values[x + 1, y + 1],
|
||||
values[x, y + 1])) :
|
||||
(new ValuesInCell(values[x, y],
|
||||
values[x + 1, y],
|
||||
values[x + 1, y + 1],
|
||||
values[x, y + 1],
|
||||
missingValue));
|
||||
|
||||
IrregularCell rect = new IrregularCell(
|
||||
grid[x, y],
|
||||
grid[x + 1, y],
|
||||
grid[x + 1, y + 1],
|
||||
grid[x, y + 1]);
|
||||
|
||||
Point point = GetPointXY(inEdge, value, vc, rect);
|
||||
|
||||
segments.StartLine(point, (value - minMax.Min) / (minMax.Max - minMax.Min), value);
|
||||
|
||||
MakeEdgePassed(inEdge, x, y);
|
||||
|
||||
//processed[x, y] = true;
|
||||
|
||||
double x2, y2;
|
||||
do
|
||||
{
|
||||
inEdge = TrackLine(inEdge, value, ref s, ref t, out x2, out y2);
|
||||
} while (inEdge != Edge.None);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private bool HasIsoline(int x, int y)
|
||||
{
|
||||
return (edges[x,y] != 0 &&
|
||||
((x < edges.GetLength(0) - 1 && edges[x+1,y] != 0) ||
|
||||
(y < edges.GetLength(1) - 1 && edges[x,y+1] != 0)));
|
||||
}
|
||||
|
||||
/// <summary>Finds isoline for specified reference value</summary>
|
||||
/// <param name="value">Reference value</param>
|
||||
private void PrepareCells(double value)
|
||||
{
|
||||
double currentRatio = (value - minMax.Min) / (minMax.Max - minMax.Min);
|
||||
|
||||
if (currentRatio < 0 || currentRatio > 1)
|
||||
return; // No contour lines for such value
|
||||
|
||||
int xSize = dataSource.Width;
|
||||
int ySize = dataSource.Height;
|
||||
int x, y;
|
||||
for (x = 0; x < xSize; x++)
|
||||
for (y = 0; y < ySize; y++)
|
||||
edges[x, y] = 0;
|
||||
|
||||
processed = new bool[xSize, ySize];
|
||||
|
||||
// Looking in boundaries.
|
||||
// left
|
||||
for (y = 1; y < ySize; y++)
|
||||
{
|
||||
if (BelongsToEdge(value, values[0, y - 1], values[0, y], true) &&
|
||||
(edges[0, y - 1] & (byte)Edge.Left) == 0)
|
||||
{
|
||||
TrackLineNonRecursive(Edge.Left, value, 0, y - 1);
|
||||
}
|
||||
}
|
||||
|
||||
// bottom
|
||||
for (x = 0; x < xSize - 1; x++)
|
||||
{
|
||||
if (BelongsToEdge(value, values[x, 0], values[x + 1, 0], true)
|
||||
&& (edges[x, 0] & (byte)Edge.Bottom) == 0)
|
||||
{
|
||||
TrackLineNonRecursive(Edge.Bottom, value, x, 0);
|
||||
};
|
||||
}
|
||||
|
||||
// right
|
||||
x = xSize - 1;
|
||||
for (y = 1; y < ySize; y++)
|
||||
{
|
||||
// Is this correct?
|
||||
//if (BelongsToEdge(value, values[0, y - 1], values[0, y], true) &&
|
||||
// (edges[0, y - 1] & (byte)Edge.Left) == 0)
|
||||
//{
|
||||
// TrackLineNonRecursive(Edge.Left, value, 0, y - 1);
|
||||
//};
|
||||
|
||||
if (BelongsToEdge(value, values[x, y - 1], values[x, y], true) &&
|
||||
(edges[x, y - 1] & (byte)Edge.Left) == 0)
|
||||
{
|
||||
TrackLineNonRecursive(Edge.Right, value, x - 1, y - 1);
|
||||
};
|
||||
}
|
||||
|
||||
// horizontals
|
||||
for (x = 1; x < xSize - 1; x++)
|
||||
for (y = 1; y < ySize - 1; y++)
|
||||
{
|
||||
if ((edges[x, y] & (byte)Edge.Bottom) == 0 &&
|
||||
BelongsToEdge(value, values[x, y], values[x + 1, y], false) &&
|
||||
!processed[x,y-1])
|
||||
{
|
||||
TrackLineNonRecursive(Edge.Top, value, x, y-1);
|
||||
}
|
||||
if ((edges[x, y] & (byte)Edge.Bottom) == 0 &&
|
||||
BelongsToEdge(value, values[x, y], values[x + 1, y], false) &&
|
||||
!processed[x,y])
|
||||
{
|
||||
TrackLineNonRecursive(Edge.Bottom, value, x, y);
|
||||
}
|
||||
if ((edges[x, y] & (byte)Edge.Left) == 0 &&
|
||||
BelongsToEdge(value, values[x, y], values[x, y - 1], false) &&
|
||||
!processed[x-1,y-1])
|
||||
{
|
||||
TrackLineNonRecursive(Edge.Right, value, x - 1, y - 1);
|
||||
}
|
||||
if ((edges[x, y] & (byte)Edge.Left) == 0 &&
|
||||
BelongsToEdge(value, values[x, y], values[x, y - 1], false) &&
|
||||
!processed[x,y-1])
|
||||
{
|
||||
TrackLineNonRecursive(Edge.Left, value, x, y - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds isoline data for 2d scalar field contained in data source.
|
||||
/// </summary>
|
||||
/// <returns>Collection of data describing built isolines.</returns>
|
||||
public IsolineCollection BuildIsoline()
|
||||
{
|
||||
VerifyDataSource();
|
||||
|
||||
segments = new IsolineCollection();
|
||||
|
||||
minMax = (Double.IsNaN(missingValue) ? dataSource.GetMinMax() : dataSource.GetMinMax(missingValue));
|
||||
|
||||
segments.Min = minMax.Min;
|
||||
segments.Max = minMax.Max;
|
||||
|
||||
if (!minMax.IsEmpty)
|
||||
{
|
||||
values = dataSource.Data;
|
||||
double[] levels = GetLevelsForIsolines();
|
||||
|
||||
foreach (double level in levels)
|
||||
{
|
||||
PrepareCells(level);
|
||||
}
|
||||
|
||||
if (segments.Lines.Count > 0 && segments.Lines[segments.Lines.Count - 1].OtherPoints.Count == 0)
|
||||
segments.Lines.RemoveAt(segments.Lines.Count - 1);
|
||||
}
|
||||
return segments;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds isoline data for the specified level in 2d scalar field.
|
||||
/// </summary>
|
||||
/// <param name="level">The level.</param>
|
||||
/// <returns></returns>
|
||||
public IsolineCollection BuildIsoline(double level)
|
||||
{
|
||||
VerifyDataSource();
|
||||
|
||||
segments = new IsolineCollection();
|
||||
|
||||
minMax = (Double.IsNaN(missingValue) ? dataSource.GetMinMax() : dataSource.GetMinMax(missingValue));
|
||||
if (!minMax.IsEmpty)
|
||||
{
|
||||
values = dataSource.Data;
|
||||
|
||||
|
||||
PrepareCells(level);
|
||||
|
||||
if (segments.Lines.Count > 0 && segments.Lines[segments.Lines.Count - 1].OtherPoints.Count == 0)
|
||||
segments.Lines.RemoveAt(segments.Lines.Count - 1);
|
||||
}
|
||||
return segments;
|
||||
}
|
||||
|
||||
private void VerifyDataSource()
|
||||
{
|
||||
if (dataSource == null)
|
||||
throw new InvalidOperationException(Strings.Exceptions.IsolinesDataSourceShouldBeSet);
|
||||
}
|
||||
|
||||
IsolineCollection segments;
|
||||
|
||||
private double[,] values;
|
||||
private byte[,] edges;
|
||||
private Point[,] grid;
|
||||
|
||||
private Range<double> minMax;
|
||||
private IDataSource2D<double> dataSource;
|
||||
/// <summary>
|
||||
/// Gets or sets the data source - 2d scalar field.
|
||||
/// </summary>
|
||||
/// <value>The data source.</value>
|
||||
public IDataSource2D<double> DataSource
|
||||
{
|
||||
get { return dataSource; }
|
||||
set
|
||||
{
|
||||
if (dataSource != value)
|
||||
{
|
||||
value.VerifyNotNull("value");
|
||||
|
||||
dataSource = value;
|
||||
grid = dataSource.Grid;
|
||||
edges = new byte[dataSource.Width, dataSource.Height];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const double shiftPercent = 0.05;
|
||||
private double[] GetLevelsForIsolines()
|
||||
{
|
||||
double[] levels;
|
||||
double min = minMax.Min;
|
||||
double max = minMax.Max;
|
||||
|
||||
double step = (max - min) / (density - 1);
|
||||
double delta = (max - min);
|
||||
|
||||
levels = new double[density];
|
||||
levels[0] = min + delta * shiftPercent;
|
||||
levels[levels.Length - 1] = max - delta * shiftPercent;
|
||||
|
||||
for (int i = 1; i < levels.Length - 1; i++)
|
||||
levels[i] = min + i * step;
|
||||
|
||||
return levels;
|
||||
}
|
||||
}
|
||||
}
|
||||
159
Charts/Isolines/IsolineCollection.cs
Normal file
159
Charts/Isolines/IsolineCollection.cs
Normal file
@@ -0,0 +1,159 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Windows;
|
||||
using System.Collections;
|
||||
|
||||
namespace Microsoft.Research.DynamicDataDisplay.Charts.Isolines
|
||||
{
|
||||
/// <summary>
|
||||
/// LevelLine contains all data for one isoline line - its start point, other points and value in field.
|
||||
/// </summary>
|
||||
public sealed class LevelLine
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the value of line in limits of [0..1].
|
||||
/// </summary>
|
||||
/// <value>The value01.</value>
|
||||
public double Value01 { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the real value of line - without scaling to [0..1] segment.
|
||||
/// </summary>
|
||||
/// <value>The real value.</value>
|
||||
public double RealValue { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the start point of line.
|
||||
/// </summary>
|
||||
/// <value>The start point.</value>
|
||||
public Point StartPoint { get; set; }
|
||||
|
||||
private readonly List<Point> otherPoints = new List<Point>();
|
||||
/// <summary>
|
||||
/// Gets other points of line, except first point.
|
||||
/// </summary>
|
||||
/// <value>The other points.</value>
|
||||
public List<Point> OtherPoints
|
||||
{
|
||||
get { return otherPoints; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all points of line, including start point.
|
||||
/// </summary>
|
||||
/// <value>All points.</value>
|
||||
public IEnumerable<Point> AllPoints
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return StartPoint;
|
||||
for (int i = 0; i < otherPoints.Count; i++)
|
||||
{
|
||||
yield return otherPoints[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all the segments of lines.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public IEnumerable<Range<Point>> GetSegments()
|
||||
{
|
||||
if (otherPoints.Count < 1)
|
||||
yield break;
|
||||
|
||||
yield return new Range<Point>(StartPoint, otherPoints[0]);
|
||||
for (int i = 1; i < otherPoints.Count; i++)
|
||||
{
|
||||
yield return new Range<Point>(otherPoints[i - 1], otherPoints[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// IsolineTextLabel contains information about one label in isoline - its text, position and rotation.
|
||||
/// </summary>
|
||||
public sealed class IsolineTextLabel
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the rotation of isoline text label.
|
||||
/// </summary>
|
||||
/// <value>The rotation.</value>
|
||||
public double Rotation { get; internal set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the text of isoline label.
|
||||
/// </summary>
|
||||
/// <value>The text.</value>
|
||||
public double Value { get; internal set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the position of isoline text label.
|
||||
/// </summary>
|
||||
/// <value>The position.</value>
|
||||
public Point Position { get; internal set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Collection which contains all data generated by <seealso cref="IsolineBuilder"/>.
|
||||
/// </summary>
|
||||
public sealed class IsolineCollection : IEnumerable<LevelLine>
|
||||
{
|
||||
private double min;
|
||||
public double Min
|
||||
{
|
||||
get { return min; }
|
||||
set { min = value; }
|
||||
}
|
||||
|
||||
private double max;
|
||||
public double Max
|
||||
{
|
||||
get { return max; }
|
||||
set { max = value; }
|
||||
}
|
||||
|
||||
private readonly List<LevelLine> lines = new List<LevelLine>();
|
||||
/// <summary>
|
||||
/// Gets the list of isoline lines.
|
||||
/// </summary>
|
||||
/// <value>The lines.</value>
|
||||
public List<LevelLine> Lines
|
||||
{
|
||||
get { return lines; }
|
||||
}
|
||||
|
||||
internal void StartLine(Point p, double value01, double realValue)
|
||||
{
|
||||
LevelLine segment = new LevelLine { StartPoint = p, Value01 = value01, RealValue = realValue };
|
||||
if (lines.Count == 0 || lines[lines.Count - 1].OtherPoints.Count > 0)
|
||||
lines.Add(segment);
|
||||
else
|
||||
lines[lines.Count - 1] = segment;
|
||||
|
||||
}
|
||||
|
||||
internal void AddPoint(Point p)
|
||||
{
|
||||
lines[lines.Count - 1].OtherPoints.Add(p);
|
||||
}
|
||||
|
||||
#region IEnumerable<LevelLine> Members
|
||||
|
||||
public IEnumerator<LevelLine> GetEnumerator()
|
||||
{
|
||||
return lines.GetEnumerator();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IEnumerable Members
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
20
Charts/Isolines/IsolineGenerationException.cs
Normal file
20
Charts/Isolines/IsolineGenerationException.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace Microsoft.Research.DynamicDataDisplay.Charts.Isolines
|
||||
{
|
||||
/// <summary>
|
||||
/// Exception that is thrown when error occurs while building isolines.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public sealed class IsolineGenerationException : Exception
|
||||
{
|
||||
internal IsolineGenerationException() { }
|
||||
internal IsolineGenerationException(string message) : base(message) { }
|
||||
internal IsolineGenerationException(string message, Exception inner) : base(message, inner) { }
|
||||
internal IsolineGenerationException(SerializationInfo info, StreamingContext context) : base(info, context) { }
|
||||
}
|
||||
}
|
||||
258
Charts/Isolines/IsolineGraph.cs
Normal file
258
Charts/Isolines/IsolineGraph.cs
Normal file
@@ -0,0 +1,258 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Shapes;
|
||||
using Microsoft.Research.DynamicDataDisplay.Charts.Isolines;
|
||||
using Microsoft.Research.DynamicDataDisplay.Common.Auxiliary;
|
||||
using Microsoft.Research.DynamicDataDisplay.Common;
|
||||
|
||||
namespace Microsoft.Research.DynamicDataDisplay.Charts
|
||||
{
|
||||
/// <summary>
|
||||
/// Draws isolines on given two-dimensional scalar data.
|
||||
/// </summary>
|
||||
public sealed class IsolineGraph : IsolineRenderer
|
||||
{
|
||||
private static Brush labelBackground = new SolidColorBrush(Color.FromArgb(130, 255, 255, 255));
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="IsolineGraph"/> class.
|
||||
/// </summary>
|
||||
public IsolineGraph()
|
||||
{
|
||||
Content = content;
|
||||
Viewport2D.SetIsContentBoundsHost(this, true);
|
||||
}
|
||||
|
||||
protected override void OnPlotterAttached()
|
||||
{
|
||||
CreateUIRepresentation();
|
||||
UpdateUIRepresentation();
|
||||
}
|
||||
|
||||
private readonly Canvas content = new Canvas();
|
||||
|
||||
protected override void UpdateDataSource()
|
||||
{
|
||||
base.UpdateDataSource();
|
||||
|
||||
CreateUIRepresentation();
|
||||
rebuildText = true;
|
||||
UpdateUIRepresentation();
|
||||
}
|
||||
|
||||
protected override void OnLineThicknessChanged()
|
||||
{
|
||||
foreach (var path in linePaths)
|
||||
{
|
||||
path.StrokeThickness = StrokeThickness;
|
||||
}
|
||||
}
|
||||
|
||||
private List<FrameworkElement> textBlocks = new List<FrameworkElement>();
|
||||
private List<Path> linePaths = new List<Path>();
|
||||
protected override void CreateUIRepresentation()
|
||||
{
|
||||
if (Plotter2D == null)
|
||||
return;
|
||||
|
||||
content.Children.Clear();
|
||||
linePaths.Clear();
|
||||
|
||||
if (Collection != null)
|
||||
{
|
||||
DataRect bounds = DataRect.Empty;
|
||||
|
||||
foreach (var line in Collection.Lines)
|
||||
{
|
||||
foreach (var point in line.AllPoints)
|
||||
{
|
||||
bounds.Union(point);
|
||||
}
|
||||
|
||||
Path path = new Path
|
||||
{
|
||||
Stroke = new SolidColorBrush(Palette.GetColor(line.Value01)),
|
||||
StrokeThickness = StrokeThickness,
|
||||
Data = CreateGeometry(line),
|
||||
Tag = line
|
||||
};
|
||||
content.Children.Add(path);
|
||||
linePaths.Add(path);
|
||||
}
|
||||
|
||||
Viewport2D.SetContentBounds(this, bounds);
|
||||
|
||||
if (DrawLabels)
|
||||
{
|
||||
var transform = Plotter2D.Viewport.Transform;
|
||||
double wayBeforeText = new Rect(new Size(2000, 2000)).ScreenToData(transform).Width;
|
||||
Annotater.WayBeforeText = wayBeforeText;
|
||||
var textLabels = Annotater.Annotate(Collection, Plotter2D.Viewport.Visible);
|
||||
|
||||
foreach (var textLabel in textLabels)
|
||||
{
|
||||
var text = CreateTextLabel(textLabel);
|
||||
content.Children.Add(text);
|
||||
textBlocks.Add(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private FrameworkElement CreateTextLabel(IsolineTextLabel textLabel)
|
||||
{
|
||||
var transform = Plotter2D.Viewport.Transform;
|
||||
Point screenPos = textLabel.Position.DataToScreen(transform);
|
||||
|
||||
double angle = textLabel.Rotation;
|
||||
if (angle < 0)
|
||||
angle += 360;
|
||||
if (135 < angle && angle < 225)
|
||||
angle -= 180;
|
||||
|
||||
string tooltip = textLabel.Value.ToString("F"); //String.Format("{0} at ({1}, {2})", textLabel.Text, textLabel.Position.X, textLabel.Position.Y);
|
||||
Grid grid = new Grid
|
||||
{
|
||||
RenderTransform = new RotateTransform(angle),
|
||||
Tag = textLabel,
|
||||
RenderTransformOrigin = new Point(0.5, 0.5),
|
||||
ToolTip = tooltip
|
||||
};
|
||||
|
||||
TextBlock res = new TextBlock
|
||||
{
|
||||
Text = textLabel.Value.ToString("F"),
|
||||
Margin = new Thickness(3,1,3,1)
|
||||
};
|
||||
|
||||
//res.Measure(SizeHelper.CreateInfiniteSize());
|
||||
|
||||
Rectangle rect = new Rectangle
|
||||
{
|
||||
Stroke = Brushes.Gray,
|
||||
Fill = labelBackground,
|
||||
RadiusX = 8,
|
||||
RadiusY = 8
|
||||
};
|
||||
|
||||
grid.Children.Add(rect);
|
||||
grid.Children.Add(res);
|
||||
|
||||
grid.Measure(SizeHelper.CreateInfiniteSize());
|
||||
|
||||
Size textSize = grid.DesiredSize;
|
||||
Point position = new Point(screenPos.X - textSize.Width / 2, screenPos.Y - textSize.Height / 2);
|
||||
|
||||
Canvas.SetLeft(grid, position.X);
|
||||
Canvas.SetTop(grid, position.Y);
|
||||
return grid;
|
||||
}
|
||||
|
||||
private Geometry CreateGeometry(LevelLine lineData)
|
||||
{
|
||||
var transform = Plotter2D.Viewport.Transform;
|
||||
|
||||
StreamGeometry geometry = new StreamGeometry();
|
||||
using (var context = geometry.Open())
|
||||
{
|
||||
context.BeginFigure(lineData.StartPoint.DataToScreen(transform), false, false);
|
||||
context.PolyLineTo(lineData.OtherPoints.DataToScreenAsList(transform), true, true);
|
||||
}
|
||||
geometry.Freeze();
|
||||
return geometry;
|
||||
}
|
||||
|
||||
private bool rebuildText = true;
|
||||
protected override void OnViewportPropertyChanged(ExtendedPropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == "Visible" || e.PropertyName == "Output")
|
||||
{
|
||||
bool isVisibleChanged = e.PropertyName == "Visible";
|
||||
DataRect prevRect = isVisibleChanged ? (DataRect)e.OldValue : new DataRect((Rect)e.OldValue);
|
||||
DataRect currRect = isVisibleChanged ? (DataRect)e.NewValue : new DataRect((Rect)e.NewValue);
|
||||
|
||||
// completely rebuild text only if width and height have changed many
|
||||
const double smallChangePercent = 0.05;
|
||||
bool widthChangedLittle = Math.Abs(currRect.Width - prevRect.Width) / currRect.Width < smallChangePercent;
|
||||
bool heightChangedLittle = Math.Abs(currRect.Height - prevRect.Height) / currRect.Height < smallChangePercent;
|
||||
|
||||
rebuildText = !(widthChangedLittle && heightChangedLittle);
|
||||
}
|
||||
UpdateUIRepresentation();
|
||||
}
|
||||
|
||||
private void UpdateUIRepresentation()
|
||||
{
|
||||
if (Plotter2D == null) return;
|
||||
|
||||
foreach (var path in linePaths)
|
||||
{
|
||||
LevelLine line = (LevelLine)path.Tag;
|
||||
path.Data = CreateGeometry(line);
|
||||
}
|
||||
|
||||
var transform = Plotter2D.Viewport.Transform;
|
||||
Rect output = Plotter2D.Viewport.Output;
|
||||
DataRect visible = Plotter2D.Viewport.Visible;
|
||||
|
||||
if (rebuildText && DrawLabels)
|
||||
{
|
||||
rebuildText = false;
|
||||
foreach (var text in textBlocks)
|
||||
{
|
||||
if (text.Visibility == Visibility.Visible)
|
||||
content.Children.Remove(text);
|
||||
}
|
||||
textBlocks.Clear();
|
||||
|
||||
double wayBeforeText = new Rect(new Size(100, 100)).ScreenToData(transform).Width;
|
||||
Annotater.WayBeforeText = wayBeforeText;
|
||||
var textLabels = Annotater.Annotate(Collection, Plotter2D.Viewport.Visible);
|
||||
foreach (var textLabel in textLabels)
|
||||
{
|
||||
var text = CreateTextLabel(textLabel);
|
||||
textBlocks.Add(text);
|
||||
if (visible.Contains(textLabel.Position))
|
||||
{
|
||||
content.Children.Add(text);
|
||||
}
|
||||
else
|
||||
{
|
||||
text.Visibility = Visibility.Hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var text in textBlocks)
|
||||
{
|
||||
IsolineTextLabel label = (IsolineTextLabel)text.Tag;
|
||||
Point screenPos = label.Position.DataToScreen(transform);
|
||||
Size textSize = text.DesiredSize;
|
||||
|
||||
Point position = new Point(screenPos.X - textSize.Width / 2, screenPos.Y - textSize.Height / 2);
|
||||
|
||||
if (output.Contains(position))
|
||||
{
|
||||
Canvas.SetLeft(text, position.X);
|
||||
Canvas.SetTop(text, position.Y);
|
||||
if (text.Visibility == Visibility.Hidden)
|
||||
{
|
||||
text.Visibility = Visibility.Visible;
|
||||
content.Children.Add(text);
|
||||
}
|
||||
}
|
||||
else if (text.Visibility == Visibility.Visible)
|
||||
{
|
||||
text.Visibility = Visibility.Hidden;
|
||||
content.Children.Remove(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
282
Charts/Isolines/IsolineGraphBase.cs
Normal file
282
Charts/Isolines/IsolineGraphBase.cs
Normal file
@@ -0,0 +1,282 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.Research.DynamicDataDisplay.Common.Palettes;
|
||||
using Microsoft.Research.DynamicDataDisplay.DataSources;
|
||||
using DataSource = Microsoft.Research.DynamicDataDisplay.DataSources.IDataSource2D<double>;
|
||||
using System.Windows;
|
||||
using Microsoft.Research.DynamicDataDisplay.Charts.Shapes;
|
||||
|
||||
namespace Microsoft.Research.DynamicDataDisplay.Charts.Isolines
|
||||
{
|
||||
public abstract class IsolineGraphBase : ContentGraph
|
||||
{
|
||||
protected IsolineGraphBase() { }
|
||||
|
||||
private IsolineCollection collection = new IsolineCollection();
|
||||
protected IsolineCollection Collection
|
||||
{
|
||||
get { return collection; }
|
||||
set { collection = value; }
|
||||
}
|
||||
|
||||
private readonly IsolineBuilder isolineBuilder = new IsolineBuilder();
|
||||
protected IsolineBuilder IsolineBuilder
|
||||
{
|
||||
get { return isolineBuilder; }
|
||||
}
|
||||
|
||||
private readonly IsolineTextAnnotater annotater = new IsolineTextAnnotater();
|
||||
protected IsolineTextAnnotater Annotater
|
||||
{
|
||||
get { return annotater; }
|
||||
}
|
||||
|
||||
#region Properties
|
||||
|
||||
#region IsolineCollection property
|
||||
|
||||
public IsolineCollection IsolineCollection
|
||||
{
|
||||
get { return (IsolineCollection)GetValue(IsolineCollectionProperty); }
|
||||
set { SetValue(IsolineCollectionProperty, value); }
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty IsolineCollectionProperty = DependencyProperty.Register(
|
||||
"IsolineCollection",
|
||||
typeof(IsolineCollection),
|
||||
typeof(IsolineGraphBase),
|
||||
new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.Inherits));
|
||||
|
||||
#endregion // end of IsolineCollection property
|
||||
|
||||
#region WayBeforeTextMultiplier
|
||||
|
||||
public double WayBeforeTextMultiplier
|
||||
{
|
||||
get { return (double)GetValue(WayBeforeTextMultiplierProperty); }
|
||||
set { SetValue(WayBeforeTextMultiplierProperty, value); }
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty WayBeforeTextMultiplierProperty = DependencyProperty.Register(
|
||||
"WayBeforeTextCoeff",
|
||||
typeof(double),
|
||||
typeof(IsolineGraphBase),
|
||||
new FrameworkPropertyMetadata(1.0, FrameworkPropertyMetadataOptions.Inherits, OnIsolinePropertyChanged));
|
||||
|
||||
#endregion // end of WayBeforeTextCoeff
|
||||
|
||||
private static void OnIsolinePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
// todo do smth here
|
||||
}
|
||||
|
||||
#region Palette property
|
||||
|
||||
public IPalette Palette
|
||||
{
|
||||
get { return (IPalette)GetValue(PaletteProperty); }
|
||||
set { SetValue(PaletteProperty, value); }
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty PaletteProperty = DependencyProperty.Register(
|
||||
"Palette",
|
||||
typeof(IPalette),
|
||||
typeof(IsolineGraphBase),
|
||||
new FrameworkPropertyMetadata(new HSBPalette(), FrameworkPropertyMetadataOptions.Inherits, OnIsolinePropertyChanged), ValidatePalette);
|
||||
|
||||
private static bool ValidatePalette(object value)
|
||||
{
|
||||
return value != null;
|
||||
}
|
||||
|
||||
#endregion // end of Palette property
|
||||
|
||||
#region DataSource property
|
||||
|
||||
public DataSource DataSource
|
||||
{
|
||||
get { return (DataSource)GetValue(DataSourceProperty); }
|
||||
set { SetValue(DataSourceProperty, value); }
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty DataSourceProperty = DependencyProperty.Register(
|
||||
"DataSource",
|
||||
typeof(DataSource),
|
||||
typeof(IsolineGraphBase),
|
||||
new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.Inherits, OnDataSourceChanged));
|
||||
|
||||
private static void OnDataSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
IsolineGraphBase owner = (IsolineGraphBase)d;
|
||||
owner.OnDataSourceChanged((DataSource)e.OldValue, (DataSource)e.NewValue);
|
||||
}
|
||||
|
||||
protected virtual void OnDataSourceChanged(IDataSource2D<double> prevDataSource, IDataSource2D<double> currDataSource)
|
||||
{
|
||||
if (prevDataSource != null)
|
||||
prevDataSource.Changed -= OnDataSourceChanged;
|
||||
if (currDataSource != null)
|
||||
currDataSource.Changed += OnDataSourceChanged;
|
||||
|
||||
UpdateDataSource();
|
||||
CreateUIRepresentation();
|
||||
|
||||
RaiseEvent(new RoutedEventArgs(BackgroundRenderer.UpdateRequested));
|
||||
}
|
||||
|
||||
#endregion // end of DataSource property
|
||||
|
||||
#region DrawLabels property
|
||||
|
||||
public bool DrawLabels
|
||||
{
|
||||
get { return (bool)GetValue(DrawLabelsProperty); }
|
||||
set { SetValue(DrawLabelsProperty, value); }
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty DrawLabelsProperty = DependencyProperty.Register(
|
||||
"DrawLabels",
|
||||
typeof(bool),
|
||||
typeof(IsolineGraphBase),
|
||||
new FrameworkPropertyMetadata(true, FrameworkPropertyMetadataOptions.Inherits, OnIsolinePropertyChanged));
|
||||
|
||||
#endregion // end of DrawLabels property
|
||||
|
||||
#region LabelStringFormat
|
||||
|
||||
public string LabelStringFormat
|
||||
{
|
||||
get { return (string)GetValue(LabelStringFormatProperty); }
|
||||
set { SetValue(LabelStringFormatProperty, value); }
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty LabelStringFormatProperty = DependencyProperty.Register(
|
||||
"LabelStringFormat",
|
||||
typeof(string),
|
||||
typeof(IsolineGraphBase),
|
||||
new FrameworkPropertyMetadata("F", FrameworkPropertyMetadataOptions.Inherits, OnIsolinePropertyChanged));
|
||||
|
||||
#endregion // end of LabelStringFormat
|
||||
|
||||
#region UseBezierCurves
|
||||
|
||||
public bool UseBezierCurves
|
||||
{
|
||||
get { return (bool)GetValue(UseBezierCurvesProperty); }
|
||||
set { SetValue(UseBezierCurvesProperty, value); }
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty UseBezierCurvesProperty = DependencyProperty.Register(
|
||||
"UseBezierCurves",
|
||||
typeof(bool),
|
||||
typeof(IsolineGraphBase),
|
||||
new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.Inherits));
|
||||
|
||||
#endregion // end of UseBezierCurves
|
||||
|
||||
#endregion // end of Properties
|
||||
|
||||
#region DataSource
|
||||
|
||||
//private DataSource dataSource = null;
|
||||
///// <summary>
|
||||
///// Gets or sets the data source.
|
||||
///// </summary>
|
||||
///// <value>The data source.</value>
|
||||
//public DataSource DataSource
|
||||
//{
|
||||
// get { return dataSource; }
|
||||
// set
|
||||
// {
|
||||
// if (dataSource != value)
|
||||
// {
|
||||
// DetachDataSource(dataSource);
|
||||
// dataSource = value;
|
||||
// AttachDataSource(dataSource);
|
||||
|
||||
// UpdateDataSource();
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
#region MissineValue property
|
||||
|
||||
public double MissingValue
|
||||
{
|
||||
get { return (double)GetValue(MissingValueProperty); }
|
||||
set { SetValue(MissingValueProperty, value); }
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty MissingValueProperty = DependencyProperty.Register(
|
||||
"MissingValue",
|
||||
typeof(double),
|
||||
typeof(IsolineGraphBase),
|
||||
new FrameworkPropertyMetadata(Double.NaN, FrameworkPropertyMetadataOptions.Inherits, OnMissingValueChanged));
|
||||
|
||||
private static void OnMissingValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
IsolineGraphBase owner = (IsolineGraphBase)d;
|
||||
owner.UpdateDataSource();
|
||||
}
|
||||
|
||||
#endregion // end of MissineValue property
|
||||
|
||||
public void SetDataSource(DataSource dataSource, double missingValue)
|
||||
{
|
||||
DataSource = dataSource;
|
||||
MissingValue = missingValue;
|
||||
|
||||
UpdateDataSource();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This method is called when data source changes.
|
||||
/// </summary>
|
||||
protected virtual void UpdateDataSource()
|
||||
{
|
||||
}
|
||||
|
||||
protected virtual void CreateUIRepresentation() { }
|
||||
|
||||
protected virtual void OnDataSourceChanged(object sender, EventArgs e)
|
||||
{
|
||||
UpdateDataSource();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region StrokeThickness
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets thickness of isoline lines.
|
||||
/// </summary>
|
||||
/// <value>The stroke thickness.</value>
|
||||
public double StrokeThickness
|
||||
{
|
||||
get { return (double)GetValue(StrokeThicknessProperty); }
|
||||
set { SetValue(StrokeThicknessProperty, value); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Identifies the StrokeThickness dependency property.
|
||||
/// </summary>
|
||||
public static readonly DependencyProperty StrokeThicknessProperty =
|
||||
DependencyProperty.Register(
|
||||
"StrokeThickness",
|
||||
typeof(double),
|
||||
typeof(IsolineGraphBase),
|
||||
new FrameworkPropertyMetadata(2.0, OnLineThicknessChanged));
|
||||
|
||||
private static void OnLineThicknessChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
IsolineGraphBase graph = (IsolineGraphBase)d;
|
||||
graph.OnLineThicknessChanged();
|
||||
}
|
||||
|
||||
protected virtual void OnLineThicknessChanged() { }
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
86
Charts/Isolines/IsolineRenderer.cs
Normal file
86
Charts/Isolines/IsolineRenderer.cs
Normal file
@@ -0,0 +1,86 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Windows.Media;
|
||||
using System.Windows;
|
||||
using System.Diagnostics;
|
||||
using Microsoft.Research.DynamicDataDisplay.Common.Auxiliary;
|
||||
using Microsoft.Research.DynamicDataDisplay.Charts.Shapes;
|
||||
using System.Windows.Threading;
|
||||
using System.Globalization;
|
||||
using Microsoft.Research.DynamicDataDisplay.Charts.NewLine;
|
||||
|
||||
namespace Microsoft.Research.DynamicDataDisplay.Charts.Isolines
|
||||
{
|
||||
public abstract class IsolineRenderer : IsolineGraphBase
|
||||
{
|
||||
protected override void UpdateDataSource()
|
||||
{
|
||||
if (DataSource != null)
|
||||
{
|
||||
IsolineBuilder.DataSource = DataSource;
|
||||
IsolineBuilder.MissingValue = MissingValue;
|
||||
Collection = IsolineBuilder.BuildIsoline();
|
||||
}
|
||||
else
|
||||
{
|
||||
Collection = null;
|
||||
}
|
||||
}
|
||||
|
||||
protected IEnumerable<double> GetAdditionalLevels(IsolineCollection collection)
|
||||
{
|
||||
var dataSource = DataSource;
|
||||
var visibleMinMax = dataSource.GetMinMax(Plotter2D.Visible);
|
||||
double totalDelta = collection.Max - collection.Min;
|
||||
double visibleMinMaxRatio = totalDelta / visibleMinMax.GetLength();
|
||||
double defaultDelta = totalDelta / 12;
|
||||
|
||||
if (true || 2 * defaultDelta < visibleMinMaxRatio)
|
||||
{
|
||||
double number = Math.Ceiling(visibleMinMaxRatio * 4);
|
||||
number = Math.Pow(2, Math.Ceiling(Math.Log(number) / Math.Log(2)));
|
||||
double delta = totalDelta / number;
|
||||
double x = collection.Min + Math.Ceiling((visibleMinMax.Min - collection.Min) / delta) * delta;
|
||||
|
||||
List<double> result = new List<double>();
|
||||
while (x < visibleMinMax.Max)
|
||||
{
|
||||
result.Add(x);
|
||||
x += delta;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
return Enumerable.Empty<double>();
|
||||
}
|
||||
|
||||
protected void RenderIsolineCollection(DrawingContext dc, double strokeThickness, IsolineCollection collection, CoordinateTransform transform)
|
||||
{
|
||||
foreach (LevelLine line in collection)
|
||||
{
|
||||
StreamGeometry lineGeometry = new StreamGeometry();
|
||||
using (var context = lineGeometry.Open())
|
||||
{
|
||||
context.BeginFigure(line.StartPoint.ViewportToScreen(transform), false, false);
|
||||
if (!UseBezierCurves)
|
||||
{
|
||||
context.PolyLineTo(line.OtherPoints.ViewportToScreen(transform).ToArray(), true, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
context.PolyBezierTo(BezierBuilder.GetBezierPoints(line.AllPoints.ViewportToScreen(transform).ToArray()).Skip(1).ToArray(), true, true);
|
||||
}
|
||||
}
|
||||
lineGeometry.Freeze();
|
||||
|
||||
Pen pen = new Pen(new SolidColorBrush(Palette.GetColor(line.Value01)), strokeThickness);
|
||||
|
||||
dc.DrawGeometry(null, pen, lineGeometry);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
74
Charts/Isolines/IsolineTextAnnotater.cs
Normal file
74
Charts/Isolines/IsolineTextAnnotater.cs
Normal file
@@ -0,0 +1,74 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Windows;
|
||||
using Microsoft.Research.DynamicDataDisplay.Common;
|
||||
|
||||
namespace Microsoft.Research.DynamicDataDisplay.Charts.Isolines
|
||||
{
|
||||
/// <summary>
|
||||
/// IsolineTextAnnotater defines methods to annotate isolines - create a list of labels with its position.
|
||||
/// </summary>
|
||||
public sealed class IsolineTextAnnotater
|
||||
{
|
||||
private double wayBeforeText = 10;
|
||||
/// <summary>
|
||||
/// Gets or sets the distance between text labels.
|
||||
/// </summary>
|
||||
public double WayBeforeText
|
||||
{
|
||||
get { return wayBeforeText; }
|
||||
set { wayBeforeText = value; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Annotates the specified isoline collection.
|
||||
/// </summary>
|
||||
/// <param name="collection">The collection.</param>
|
||||
/// <param name="visible">The visible rectangle.</param>
|
||||
/// <returns></returns>
|
||||
public Collection<IsolineTextLabel> Annotate(IsolineCollection collection, DataRect visible)
|
||||
{
|
||||
Collection<IsolineTextLabel> res = new Collection<IsolineTextLabel>();
|
||||
|
||||
foreach (var line in collection.Lines)
|
||||
{
|
||||
double way = 0;
|
||||
|
||||
var forwardSegments = line.GetSegments();
|
||||
var forwardEnumerator = forwardSegments.GetEnumerator();
|
||||
forwardEnumerator.MoveNext();
|
||||
|
||||
foreach (var segment in line.GetSegments())
|
||||
{
|
||||
bool hasForwardSegment = forwardEnumerator.MoveNext();
|
||||
|
||||
double length = segment.GetLength();
|
||||
way += length;
|
||||
if (way > wayBeforeText)
|
||||
{
|
||||
way = 0;
|
||||
|
||||
var rotation = (segment.Max - segment.Min).ToAngle();
|
||||
if (hasForwardSegment)
|
||||
{
|
||||
var forwardSegment = forwardEnumerator.Current;
|
||||
rotation = (rotation + (forwardSegment.Max - forwardSegment.Min).ToAngle()) / 2;
|
||||
}
|
||||
|
||||
res.Add(new IsolineTextLabel
|
||||
{
|
||||
Value = line.RealValue,
|
||||
Position = segment.Max,
|
||||
Rotation = rotation
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
}
|
||||
}
|
||||
19
Charts/Isolines/IsolineTrackingGraph.xaml
Normal file
19
Charts/Isolines/IsolineTrackingGraph.xaml
Normal file
@@ -0,0 +1,19 @@
|
||||
<d3:IsolineGraphBase x:Class="Microsoft.Research.DynamicDataDisplay.Charts.IsolineTrackingGraph"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:d3="clr-namespace:Microsoft.Research.DynamicDataDisplay.Charts.Isolines"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<d3:IsolineGraphBase.Style>
|
||||
<Style TargetType="{x:Type d3:IsolineGraphBase}">
|
||||
<Setter Property="Cursor" Value="Cross"/>
|
||||
<Setter Property="StrokeThickness" Value="3"/>
|
||||
</Style>
|
||||
</d3:IsolineGraphBase.Style>
|
||||
|
||||
<Canvas Name="content" Background="Transparent">
|
||||
<Grid Name="labelGrid" Visibility="Hidden" Panel.ZIndex="1">
|
||||
<Rectangle RadiusX="10" RadiusY="10" Stroke="DarkGray"
|
||||
StrokeThickness="1" Fill="#AAFFFFFF"/>
|
||||
<TextBlock Name="textBlock" Margin="10,5,10,5"/>
|
||||
</Grid>
|
||||
</Canvas>
|
||||
</d3:IsolineGraphBase>
|
||||
255
Charts/Isolines/IsolineTrackingGraph.xaml.cs
Normal file
255
Charts/Isolines/IsolineTrackingGraph.xaml.cs
Normal file
@@ -0,0 +1,255 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Navigation;
|
||||
using System.Windows.Shapes;
|
||||
using Microsoft.Research.DynamicDataDisplay.Charts.Isolines;
|
||||
using Microsoft.Research.DynamicDataDisplay;
|
||||
using Microsoft.Research.DynamicDataDisplay.Common;
|
||||
|
||||
namespace Microsoft.Research.DynamicDataDisplay.Charts
|
||||
{
|
||||
/// <summary>
|
||||
/// Draws one isoline line through mouse position.
|
||||
/// </summary>
|
||||
public partial class IsolineTrackingGraph : IsolineGraphBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="IsolineTrackingGraph"/> class.
|
||||
/// </summary>
|
||||
public IsolineTrackingGraph()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private Style pathStyle = null;
|
||||
/// <summary>
|
||||
/// Gets or sets style, applied to line path.
|
||||
/// </summary>
|
||||
/// <value>The path style.</value>
|
||||
public Style PathStyle
|
||||
{
|
||||
get { return pathStyle; }
|
||||
set
|
||||
{
|
||||
pathStyle = value;
|
||||
foreach (var path in addedPaths)
|
||||
{
|
||||
path.Style = pathStyle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Point prevMousePos;
|
||||
|
||||
protected override void OnPlotterAttached()
|
||||
{
|
||||
UIElement parent = (UIElement)Parent;
|
||||
parent.MouseMove += parent_MouseMove;
|
||||
|
||||
UpdateUIRepresentation();
|
||||
}
|
||||
|
||||
protected override void OnPlotterDetaching()
|
||||
{
|
||||
UIElement parent = (UIElement)Parent;
|
||||
parent.MouseMove -= parent_MouseMove;
|
||||
}
|
||||
|
||||
private void parent_MouseMove(object sender, MouseEventArgs e)
|
||||
{
|
||||
Point mousePos = e.GetPosition(this);
|
||||
if (mousePos != prevMousePos)
|
||||
{
|
||||
prevMousePos = mousePos;
|
||||
|
||||
UpdateUIRepresentation();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void UpdateDataSource()
|
||||
{
|
||||
IsolineBuilder.DataSource = DataSource;
|
||||
|
||||
UpdateUIRepresentation();
|
||||
}
|
||||
|
||||
protected override void OnViewportPropertyChanged(ExtendedPropertyChangedEventArgs e)
|
||||
{
|
||||
UpdateUIRepresentation();
|
||||
}
|
||||
|
||||
private readonly List<Path> addedPaths = new List<Path>();
|
||||
private Vector labelShift = new Vector(3, 3);
|
||||
private void UpdateUIRepresentation()
|
||||
{
|
||||
if (Plotter2D == null)
|
||||
return;
|
||||
|
||||
foreach (var path in addedPaths)
|
||||
{
|
||||
content.Children.Remove(path);
|
||||
}
|
||||
addedPaths.Clear();
|
||||
|
||||
if (DataSource == null)
|
||||
{
|
||||
labelGrid.Visibility = Visibility.Hidden;
|
||||
return;
|
||||
}
|
||||
|
||||
Rect output = Plotter2D.Viewport.Output;
|
||||
|
||||
Point mousePos = Mouse.GetPosition(this);
|
||||
if (!output.Contains(mousePos)) return;
|
||||
|
||||
var transform = Plotter2D.Viewport.Transform;
|
||||
Point visiblePoint = mousePos.ScreenToData(transform);
|
||||
DataRect visible = Plotter2D.Viewport.Visible;
|
||||
|
||||
double isolineLevel;
|
||||
if (Search(visiblePoint, out isolineLevel))
|
||||
{
|
||||
var collection = IsolineBuilder.BuildIsoline(isolineLevel);
|
||||
|
||||
string format = "G3";
|
||||
if (Math.Abs(isolineLevel) < 1000)
|
||||
format = "F";
|
||||
|
||||
textBlock.Text = isolineLevel.ToString(format);
|
||||
|
||||
double x = mousePos.X + labelShift.X;
|
||||
if (x + labelGrid.ActualWidth > output.Right)
|
||||
x = mousePos.X - labelShift.X - labelGrid.ActualWidth;
|
||||
double y = mousePos.Y - labelShift.Y - labelGrid.ActualHeight;
|
||||
if (y < output.Top)
|
||||
y = mousePos.Y + labelShift.Y;
|
||||
|
||||
Canvas.SetLeft(labelGrid, x);
|
||||
Canvas.SetTop(labelGrid, y);
|
||||
|
||||
foreach (LevelLine segment in collection.Lines)
|
||||
{
|
||||
StreamGeometry streamGeom = new StreamGeometry();
|
||||
using (StreamGeometryContext context = streamGeom.Open())
|
||||
{
|
||||
Point startPoint = segment.StartPoint.DataToScreen(transform);
|
||||
var otherPoints = segment.OtherPoints.DataToScreenAsList(transform);
|
||||
context.BeginFigure(startPoint, false, false);
|
||||
context.PolyLineTo(otherPoints, true, true);
|
||||
}
|
||||
|
||||
Path path = new Path
|
||||
{
|
||||
Stroke = new SolidColorBrush(Palette.GetColor(segment.Value01)),
|
||||
Data = streamGeom,
|
||||
Style = pathStyle
|
||||
};
|
||||
content.Children.Add(path);
|
||||
addedPaths.Add(path);
|
||||
|
||||
labelGrid.Visibility = Visibility.Visible;
|
||||
|
||||
Binding pathBinding = new Binding { Path = new PropertyPath("StrokeThickness"), Source = this };
|
||||
path.SetBinding(Path.StrokeThicknessProperty, pathBinding);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
labelGrid.Visibility = Visibility.Hidden;
|
||||
}
|
||||
}
|
||||
|
||||
int foundI = 0;
|
||||
int foundJ = 0;
|
||||
Quad foundQuad = null;
|
||||
private bool Search(Point pt, out double foundVal)
|
||||
{
|
||||
var grid = DataSource.Grid;
|
||||
|
||||
foundVal = 0;
|
||||
|
||||
int width = DataSource.Width;
|
||||
int height = DataSource.Height;
|
||||
bool found = false;
|
||||
int i = 0, j = 0;
|
||||
for (i = 0; i < width - 1; i++)
|
||||
{
|
||||
for (j = 0; j < height - 1; j++)
|
||||
{
|
||||
Quad quad = new Quad(
|
||||
grid[i, j],
|
||||
grid[i, j + 1],
|
||||
grid[i + 1, j + 1],
|
||||
grid[i + 1, j]);
|
||||
if (quad.Contains(pt))
|
||||
{
|
||||
found = true;
|
||||
foundQuad = quad;
|
||||
foundI = i;
|
||||
foundJ = j;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found) break;
|
||||
}
|
||||
if (!found)
|
||||
{
|
||||
foundQuad = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
var data = DataSource.Data;
|
||||
|
||||
double x = pt.X;
|
||||
double y = pt.Y;
|
||||
Vector A = grid[i, j + 1].ToVector(); // @TODO: in common case add a sorting of points:
|
||||
Vector B = grid[i + 1, j + 1].ToVector(); // maxA ___K___ B
|
||||
Vector C = grid[i + 1, j].ToVector(); // | |
|
||||
Vector D = grid[i, j].ToVector(); // M P N
|
||||
double a = data[i, j + 1]; // | |
|
||||
double b = data[i + 1, j + 1]; // В ___L____Сmin
|
||||
double c = data[i + 1, j];
|
||||
double d = data[i, j];
|
||||
|
||||
Vector K, L;
|
||||
double k, l;
|
||||
if (x >= A.X)
|
||||
k = Interpolate(A, B, a, b, K = new Vector(x, GetY(A, B, x)));
|
||||
else
|
||||
k = Interpolate(D, A, d, a, K = new Vector(x, GetY(D, A, x)));
|
||||
|
||||
if (x >= C.X)
|
||||
l = Interpolate(C, B, c, b, L = new Vector(x, GetY(C, B, x)));
|
||||
else
|
||||
l = Interpolate(D, C, d, c, L = new Vector(x, GetY(D, C, x)));
|
||||
|
||||
foundVal = Interpolate(L, K, l, k, new Vector(x, y));
|
||||
return !Double.IsNaN(foundVal);
|
||||
}
|
||||
|
||||
private double Interpolate(Vector v0, Vector v1, double u0, double u1, Vector a)
|
||||
{
|
||||
Vector l1 = a - v0;
|
||||
Vector l = v1 - v0;
|
||||
|
||||
double res = (u1 - u0) / l.Length * l1.Length + u0;
|
||||
return res;
|
||||
}
|
||||
|
||||
private double GetY(Vector v0, Vector v1, double x)
|
||||
{
|
||||
double res = v0.Y + (v1.Y - v0.Y) / (v1.X - v0.X) * (x - v0.X);
|
||||
return res;
|
||||
}
|
||||
}
|
||||
}
|
||||
63
Charts/Isolines/Quad.cs
Normal file
63
Charts/Isolines/Quad.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
using System;
|
||||
using Microsoft.Research.DynamicDataDisplay.Common.Auxiliary;
|
||||
using System.Windows;
|
||||
|
||||
namespace Microsoft.Research.DynamicDataDisplay.Charts.Isolines
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents quadrangle; its points are arranged by round in one direction.
|
||||
/// </summary>
|
||||
internal sealed class Quad
|
||||
{
|
||||
private readonly Point v00;
|
||||
public Point V00
|
||||
{
|
||||
get { return v00; }
|
||||
}
|
||||
|
||||
private readonly Point v01;
|
||||
public Point V01
|
||||
{
|
||||
get { return v01; }
|
||||
}
|
||||
|
||||
private readonly Point v10;
|
||||
public Point V10
|
||||
{
|
||||
get { return v10; }
|
||||
}
|
||||
|
||||
private readonly Point v11;
|
||||
public Point V11
|
||||
{
|
||||
get { return v11; }
|
||||
}
|
||||
|
||||
public Quad(Point v00, Point v01, Point v11, Point v10)
|
||||
{
|
||||
DebugVerify.IsNotNaN(v00);
|
||||
DebugVerify.IsNotNaN(v01);
|
||||
DebugVerify.IsNotNaN(v11);
|
||||
DebugVerify.IsNotNaN(v10);
|
||||
|
||||
this.v00 = v00;
|
||||
this.v01 = v01;
|
||||
this.v10 = v10;
|
||||
this.v11 = v11;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether this quad contains the specified point.
|
||||
/// </summary>
|
||||
/// <param name="v">The point</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if quad contains the specified point; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
public bool Contains(Point pt)
|
||||
{
|
||||
// breaking quad into 2 triangles,
|
||||
// points contains in quad, if it contains in at least one half-triangle of it.
|
||||
return TriangleMath.TriangleContains(v00, v01, v11, pt) || TriangleMath.TriangleContains(v00, v10, v11, pt);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user