AirScout/OxyPlot/Source/OxyPlot.Windows/RenderContext.cs

842 wiersze
29 KiB
C#

// --------------------------------------------------------------------------------------------------------------------
// <copyright file="RenderContext.cs" company="OxyPlot">
// Copyright (c) 2014 OxyPlot contributors
// </copyright>
// <summary>
// Implements <see cref="IRenderContext" /> for <see cref="Canvas" />.
// </summary>
// --------------------------------------------------------------------------------------------------------------------
namespace OxyPlot.Windows
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Threading.Tasks;
using global::Windows.Foundation;
using global::Windows.Storage.Streams;
using global::Windows.UI.Text;
using global::Windows.UI.Xaml;
using global::Windows.UI.Xaml.Controls;
using global::Windows.UI.Xaml.Media;
using global::Windows.UI.Xaml.Media.Imaging;
using global::Windows.UI.Xaml.Shapes;
using Path = global::Windows.UI.Xaml.Shapes.Path;
/// <summary>
/// Implements <see cref="IRenderContext" /> for <see cref="Canvas" />.
/// </summary>
public class RenderContext : IRenderContext
{
/// <summary>
/// The brush cache.
/// </summary>
private readonly Dictionary<OxyColor, Brush> brushCache = new Dictionary<OxyColor, Brush>();
/// <summary>
/// The canvas.
/// </summary>
private readonly Canvas canvas;
/// <summary>
/// The images in use
/// </summary>
private readonly HashSet<OxyImage> imagesInUse = new HashSet<OxyImage>();
/// <summary>
/// The image cache
/// </summary>
private readonly Dictionary<OxyImage, BitmapSource> imageCache = new Dictionary<OxyImage, BitmapSource>();
/// <summary>
/// The current tool tip
/// </summary>
private string currentToolTip;
/// <summary>
/// The clip rectangle.
/// </summary>
private Rect clipRect;
/// <summary>
/// The clip flag.
/// </summary>
private bool clip;
/// <summary>
/// Initializes a new instance of the <see cref="RenderContext" /> class.
/// </summary>
/// <param name="canvas">The canvas.</param>
public RenderContext(Canvas canvas)
{
this.canvas = canvas;
this.Width = canvas.ActualWidth;
this.Height = canvas.ActualHeight;
this.RendersToScreen = true;
}
/// <summary>
/// Gets the height.
/// </summary>
/// <value>The height.</value>
public double Height { get; private set; }
/// <summary>
/// Gets a value indicating whether to paint the background.
/// </summary>
/// <value><c>true</c> if the background should be painted; otherwise, <c>false</c>.</value>
public bool PaintBackground
{
get
{
return false;
}
}
/// <summary>
/// Gets the width.
/// </summary>
/// <value>The width.</value>
public double Width { get; private set; }
/// <summary>
/// Gets or sets a value indicating whether the context renders to screen.
/// </summary>
/// <value><c>true</c> if the context renders to screen; otherwise, <c>false</c>.</value>
public bool RendersToScreen { get; set; }
/// <summary>
/// Draws an ellipse.
/// </summary>
/// <param name="rect">The rectangle.</param>
/// <param name="fill">The fill color.</param>
/// <param name="stroke">The stroke color.</param>
/// <param name="thickness">The thickness.</param>
public void DrawEllipse(OxyRect rect, OxyColor fill, OxyColor stroke, double thickness)
{
var el = new Ellipse
{
CompositeMode = ElementCompositeMode.SourceOver
};
if (stroke.IsVisible())
{
el.Stroke = new SolidColorBrush(stroke.ToColor());
el.StrokeThickness = thickness;
}
if (fill.IsVisible())
{
el.Fill = new SolidColorBrush(fill.ToColor());
}
el.Width = rect.Width;
el.Height = rect.Height;
Canvas.SetLeft(el, rect.Left);
Canvas.SetTop(el, rect.Top);
this.Add(el, rect.Left, rect.Top);
}
/// <summary>
/// Draws the collection of ellipses, where all have the same stroke and fill.
/// This performs better than calling DrawEllipse multiple times.
/// </summary>
/// <param name="rectangles">The rectangles.</param>
/// <param name="fill">The fill color.</param>
/// <param name="stroke">The stroke color.</param>
/// <param name="thickness">The stroke thickness.</param>
public void DrawEllipses(IList<OxyRect> rectangles, OxyColor fill, OxyColor stroke, double thickness)
{
var path = new Path
{
CompositeMode = ElementCompositeMode.SourceOver
};
this.SetStroke(path, stroke, thickness);
if (fill.IsVisible())
{
path.Fill = this.GetCachedBrush(fill);
}
var gg = new GeometryGroup { FillRule = FillRule.Nonzero };
foreach (var rect in rectangles)
{
gg.Children.Add(
new EllipseGeometry
{
Center = new Point(rect.Left + (rect.Width / 2), rect.Top + (rect.Height / 2)),
RadiusX = rect.Width / 2,
RadiusY = rect.Height / 2
});
}
path.Data = gg;
this.Add(path);
}
/// <summary>
/// Draws the polyline from the specified points.
/// </summary>
/// <param name="points">The points.</param>
/// <param name="stroke">The stroke color.</param>
/// <param name="thickness">The stroke thickness.</param>
/// <param name="dashArray">The dash array.</param>
/// <param name="lineJoin">The line join type.</param>
/// <param name="aliased">if set to <c>true</c> the shape will be aliased.</param>
public void DrawLine(
IList<ScreenPoint> points,
OxyColor stroke,
double thickness,
double[] dashArray,
LineJoin lineJoin,
bool aliased)
{
var e = new Polyline
{
CompositeMode = ElementCompositeMode.SourceOver
};
this.SetStroke(e, stroke, thickness, lineJoin, dashArray, aliased);
var pc = new PointCollection();
foreach (var p in points)
{
pc.Add(p.ToPoint(aliased));
}
e.Points = pc;
this.Add(e);
}
/// <summary>
/// Draws the multiple line segments defined by points (0,1) (2,3) (4,5) etc.
/// This should have better performance than calling DrawLine for each segment.
/// </summary>
/// <param name="points">The points.</param>
/// <param name="stroke">The stroke color.</param>
/// <param name="thickness">The stroke thickness.</param>
/// <param name="dashArray">The dash array.</param>
/// <param name="lineJoin">The line join type.</param>
/// <param name="aliased">if set to <c>true</c> the shape will be aliased.</param>
public void DrawLineSegments(
IList<ScreenPoint> points,
OxyColor stroke,
double thickness,
double[] dashArray,
LineJoin lineJoin,
bool aliased)
{
var path = new Path
{
CompositeMode = ElementCompositeMode.SourceOver
};
this.SetStroke(path, stroke, thickness, lineJoin, dashArray, aliased);
var pg = new PathGeometry();
for (int i = 0; i + 1 < points.Count; i += 2)
{
// if (points[i].Y==points[i+1].Y)
// {
// var line = new Line();
// line.X1 = 0.5+(int)points[i].X;
// line.X2 = 0.5+(int)points[i+1].X;
// line.Y1 = 0.5+(int)points[i].Y;
// line.Y2 = 0.5+(int)points[i+1].Y;
// SetStroke(line, OxyColors.DarkRed, thickness, lineJoin, dashArray, aliased);
// Add(line);
// continue;
// }
var figure = new PathFigure { StartPoint = points[i].ToPoint(aliased), IsClosed = false };
figure.Segments.Add(new LineSegment { Point = points[i + 1].ToPoint(aliased) });
pg.Figures.Add(figure);
}
path.Data = pg;
this.Add(path);
}
/// <summary>
/// Draws the polygon from the specified points. The polygon can have stroke and/or fill.
/// </summary>
/// <param name="points">The points.</param>
/// <param name="fill">The fill color.</param>
/// <param name="stroke">The stroke color.</param>
/// <param name="thickness">The stroke thickness.</param>
/// <param name="dashArray">The dash array.</param>
/// <param name="lineJoin">The line join type.</param>
/// <param name="aliased">if set to <c>true</c> the shape will be aliased.</param>
public void DrawPolygon(
IList<ScreenPoint> points,
OxyColor fill,
OxyColor stroke,
double thickness,
double[] dashArray,
LineJoin lineJoin,
bool aliased)
{
var po = new Polygon
{
CompositeMode = ElementCompositeMode.SourceOver
};
this.SetStroke(po, stroke, thickness, lineJoin, dashArray, aliased);
if (fill.IsVisible())
{
po.Fill = this.GetCachedBrush(fill);
}
var pc = new PointCollection();
foreach (var p in points)
{
pc.Add(p.ToPoint(aliased));
}
po.Points = pc;
this.Add(po);
}
/// <summary>
/// Draws a collection of polygons, where all polygons have the same stroke and fill.
/// This performs better than calling DrawPolygon multiple times.
/// </summary>
/// <param name="polygons">The polygons.</param>
/// <param name="fill">The fill color.</param>
/// <param name="stroke">The stroke color.</param>
/// <param name="thickness">The stroke thickness.</param>
/// <param name="dashArray">The dash array.</param>
/// <param name="lineJoin">The line join type.</param>
/// <param name="aliased">if set to <c>true</c> the shape will be aliased.</param>
public void DrawPolygons(
IList<IList<ScreenPoint>> polygons,
OxyColor fill,
OxyColor stroke,
double thickness,
double[] dashArray,
LineJoin lineJoin,
bool aliased)
{
var path = new Path
{
CompositeMode = ElementCompositeMode.SourceOver
};
this.SetStroke(path, stroke, thickness, lineJoin, dashArray, aliased);
if (fill.IsVisible())
{
path.Fill = this.GetCachedBrush(fill);
}
var pg = new PathGeometry { FillRule = FillRule.Nonzero };
foreach (var polygon in polygons)
{
var figure = new PathFigure { IsClosed = true };
bool first = true;
foreach (var p in polygon)
{
if (first)
{
figure.StartPoint = p.ToPoint(aliased);
first = false;
}
else
{
figure.Segments.Add(new LineSegment { Point = p.ToPoint(aliased) });
}
}
pg.Figures.Add(figure);
}
path.Data = pg;
this.Add(path);
}
/// <summary>
/// Draws the rectangle.
/// </summary>
/// <param name="rect">The rectangle.</param>
/// <param name="fill">The fill color.</param>
/// <param name="stroke">The stroke color.</param>
/// <param name="thickness">The stroke thickness.</param>
public void DrawRectangle(OxyRect rect, OxyColor fill, OxyColor stroke, double thickness)
{
var el = new Rectangle
{
CompositeMode = ElementCompositeMode.SourceOver
};
if (stroke.IsVisible())
{
el.Stroke = new SolidColorBrush(stroke.ToColor());
el.StrokeThickness = thickness;
}
if (fill.IsVisible())
{
el.Fill = new SolidColorBrush(fill.ToColor());
}
el.Width = rect.Width;
el.Height = rect.Height;
Canvas.SetLeft(el, rect.Left);
Canvas.SetTop(el, rect.Top);
this.Add(el, rect.Left, rect.Top);
}
/// <summary>
/// Draws a collection of rectangles, where all have the same stroke and fill.
/// This performs better than calling DrawRectangle multiple times.
/// </summary>
/// <param name="rectangles">The rectangles.</param>
/// <param name="fill">The fill color.</param>
/// <param name="stroke">The stroke color.</param>
/// <param name="thickness">The stroke thickness.</param>
public void DrawRectangles(IList<OxyRect> rectangles, OxyColor fill, OxyColor stroke, double thickness)
{
var path = new Path
{
CompositeMode = ElementCompositeMode.SourceOver
};
this.SetStroke(path, stroke, thickness);
if (fill.IsVisible())
{
path.Fill = this.GetCachedBrush(fill);
}
var gg = new GeometryGroup { FillRule = FillRule.Nonzero };
foreach (var rect in rectangles)
{
gg.Children.Add(new RectangleGeometry { Rect = rect.ToRect(true) });
}
path.Data = gg;
this.Add(path);
}
/// <summary>
/// Draws the text.
/// </summary>
/// <param name="p">The position.</param>
/// <param name="text">The text.</param>
/// <param name="fill">The fill color.</param>
/// <param name="fontFamily">The font family.</param>
/// <param name="fontSize">Size of the font.</param>
/// <param name="fontWeight">The font weight.</param>
/// <param name="rotate">The rotation angle.</param>
/// <param name="halign">The horizontal alignment.</param>
/// <param name="valign">The vertical alignment.</param>
/// <param name="maxSize">The maximum size of the text.</param>
public void DrawText(
ScreenPoint p,
string text,
OxyColor fill,
string fontFamily,
double fontSize,
double fontWeight,
double rotate,
OxyPlot.HorizontalAlignment halign,
OxyPlot.VerticalAlignment valign,
OxySize? maxSize)
{
var tb = new TextBlock { Text = text, Foreground = new SolidColorBrush(fill.ToColor()) };
// tb.SetValue(TextOptions.TextHintingModeProperty, TextHintingMode.Animated);
if (fontFamily != null)
{
tb.FontFamily = new FontFamily(fontFamily);
}
if (fontSize > 0)
{
tb.FontSize = fontSize;
}
tb.FontWeight = GetFontWeight(fontWeight);
tb.Measure(new Size(1000, 1000));
var size = new Size(tb.ActualWidth, tb.ActualHeight);
if (maxSize != null)
{
if (size.Width > maxSize.Value.Width)
{
size.Width = maxSize.Value.Width;
}
if (size.Height > maxSize.Value.Height)
{
size.Height = maxSize.Value.Height;
}
tb.Clip = new RectangleGeometry { Rect = new Rect(0, 0, size.Width, size.Height) };
}
double dx = 0;
if (halign == OxyPlot.HorizontalAlignment.Center)
{
dx = -size.Width / 2;
}
if (halign == OxyPlot.HorizontalAlignment.Right)
{
dx = -size.Width;
}
double dy = 0;
if (valign == OxyPlot.VerticalAlignment.Middle)
{
dy = -size.Height / 2;
}
if (valign == OxyPlot.VerticalAlignment.Bottom)
{
dy = -size.Height;
}
var transform = new TransformGroup();
transform.Children.Add(new TranslateTransform { X = (int)dx, Y = (int)dy });
if (!rotate.Equals(0))
{
transform.Children.Add(new RotateTransform { Angle = rotate });
}
transform.Children.Add(new TranslateTransform { X = (int)p.X, Y = (int)p.Y });
tb.RenderTransform = transform;
this.ApplyTooltip(tb);
if (this.clip)
{
// add a clipping container that is not rotated
var c = new Canvas();
c.Children.Add(tb);
this.Add(c);
}
else
{
this.Add(tb);
}
}
/// <summary>
/// Measures the text.
/// </summary>
/// <param name="text">The text.</param>
/// <param name="fontFamily">The font family.</param>
/// <param name="fontSize">Size of the font.</param>
/// <param name="fontWeight">The font weight.</param>
/// <returns>The text size.</returns>
public OxySize MeasureText(string text, string fontFamily, double fontSize, double fontWeight)
{
if (string.IsNullOrEmpty(text))
{
return OxySize.Empty;
}
var tb = new TextBlock { Text = text };
if (fontFamily != null)
{
tb.FontFamily = new FontFamily(fontFamily);
}
if (fontSize > 0)
{
tb.FontSize = fontSize;
}
tb.FontWeight = GetFontWeight(fontWeight);
tb.Measure(new Size(1000, 1000));
return new OxySize(tb.ActualWidth, tb.ActualHeight);
}
/// <summary>
/// Sets the tool tip for the following items.
/// </summary>
/// <param name="text">The text in the tooltip.</param>
public void SetToolTip(string text)
{
this.currentToolTip = text;
}
/// <summary>
/// Draws the specified portion of the specified <see cref="OxyImage" /> at the specified location and with the specified size.
/// </summary>
/// <param name="source">The source.</param>
/// <param name="srcX">The x-coordinate of the upper-left corner of the portion of the source image to draw.</param>
/// <param name="srcY">The y-coordinate of the upper-left corner of the portion of the source image to draw.</param>
/// <param name="srcWidth">Width of the portion of the source image to draw.</param>
/// <param name="srcHeight">Height of the portion of the source image to draw.</param>
/// <param name="destX">The x-coordinate of the upper-left corner of drawn image.</param>
/// <param name="destY">The y-coordinate of the upper-left corner of drawn image.</param>
/// <param name="destWidth">The width of the drawn image.</param>
/// <param name="destHeight">The height of the drawn image.</param>
/// <param name="opacity">The opacity.</param>
/// <param name="interpolate">interpolate if set to <c>true</c>.</param>
public void DrawImage(
OxyImage source,
double srcX,
double srcY,
double srcWidth,
double srcHeight,
double destX,
double destY,
double destWidth,
double destHeight,
double opacity,
bool interpolate)
{
if (destWidth <= 0 || destHeight <= 0 || srcWidth <= 0 || srcHeight <= 0)
{
return;
}
var image = new Image();
var bmp = this.GetImageSource(source);
if (srcX.Equals(0) && srcY.Equals(0) && srcWidth.Equals(bmp.PixelWidth) && srcHeight.Equals(bmp.PixelHeight))
{
// do not crop
}
else
{
// TODO: cropped image not available in Silverlight??
// bmp = new CroppedBitmap(bmp, new Int32Rect((int)srcX, (int)srcY, (int)srcWidth, (int)srcHeight));
return;
}
image.Opacity = opacity;
image.Width = destWidth;
image.Height = destHeight;
image.Stretch = Stretch.Fill;
// TODO: not available in Silverlight??
// RenderOptions.SetBitmapScalingMode(image, interpolate ? BitmapScalingMode.HighQuality : BitmapScalingMode.NearestNeighbor);
// Canvas.SetLeft(image, x);
// Canvas.SetTop(image, y);
image.RenderTransform = new TranslateTransform { X = destX, Y = destY };
image.Source = bmp;
this.ApplyTooltip(image);
this.Add(image, destX, destY);
}
/// <summary>
/// Sets the clipping rectangle.
/// </summary>
/// <param name="clippingRect">The clipping rectangle.</param>
/// <returns>True if the clipping rectangle was set.</returns>
public bool SetClip(OxyRect clippingRect)
{
this.clipRect = clippingRect.ToRect(false);
this.clip = true;
return true;
}
/// <summary>
/// Resets the clipping rectangle.
/// </summary>
public void ResetClip()
{
this.clip = false;
}
/// <summary>
/// Cleans up resources not in use.
/// </summary>
/// <remarks>This method is called at the end of each rendering.</remarks>
public void CleanUp()
{
// Find the images in the cache that has not been used since last call to this method
var imagesToRelease = this.imageCache.Keys.Where(i => !this.imagesInUse.Contains(i)).ToList();
// Remove the images from the cache
foreach (var i in imagesToRelease)
{
this.imageCache.Remove(i);
}
this.imagesInUse.Clear();
}
/// <summary>
/// Creates the dash array collection.
/// </summary>
/// <param name="dashArray">The dash array.</param>
/// <returns>A DoubleCollection.</returns>
private static DoubleCollection CreateDashArrayCollection(IEnumerable<double> dashArray)
{
var dac = new DoubleCollection();
foreach (double v in dashArray)
{
dac.Add(v);
}
return dac;
}
/// <summary>
/// Gets the font weight.
/// </summary>
/// <param name="fontWeight">The font weight.</param>
/// <returns>A <see cref="FontWeight" /></returns>
private static FontWeight GetFontWeight(double fontWeight)
{
return fontWeight > OxyPlot.FontWeights.Normal ? FontWeights.Bold : FontWeights.Normal;
}
/// <summary>
/// Adds the specified element to the canvas.
/// </summary>
/// <param name="element">The element.</param>
/// <param name="clipOffsetX">The clip offset X.</param>
/// <param name="clipOffsetY">The clip offset Y.</param>
private void Add(UIElement element, double clipOffsetX = 0, double clipOffsetY = 0)
{
if (this.clip)
{
this.ApplyClip(element, clipOffsetX, clipOffsetY);
}
this.canvas.Children.Add(element);
}
/// <summary>
/// Applies the tooltip to the specified element.
/// </summary>
/// <param name="element">The element.</param>
private void ApplyTooltip(DependencyObject element)
{
if (!string.IsNullOrEmpty(this.currentToolTip))
{
ToolTipService.SetToolTip(element, this.currentToolTip);
}
}
/// <summary>
/// Gets the cached brush.
/// </summary>
/// <param name="stroke">The stroke.</param>
/// <returns>The brush.</returns>
private Brush GetCachedBrush(OxyColor stroke)
{
Brush brush;
if (!this.brushCache.TryGetValue(stroke, out brush))
{
brush = new SolidColorBrush(stroke.ToColor());
this.brushCache.Add(stroke, brush);
}
return brush;
}
/// <summary>
/// Sets the stroke of the specified shape.
/// </summary>
/// <param name="shape">The shape.</param>
/// <param name="stroke">The stroke.</param>
/// <param name="thickness">The thickness.</param>
/// <param name="lineJoin">The line join.</param>
/// <param name="dashArray">The dash array.</param>
/// <param name="aliased">aliased if set to <c>true</c>.</param>
private void SetStroke(
Shape shape,
OxyColor stroke,
double thickness,
LineJoin lineJoin = LineJoin.Miter,
IEnumerable<double> dashArray = null,
bool aliased = false)
{
if (stroke.IsVisible() && thickness > 0)
{
shape.Stroke = this.GetCachedBrush(stroke);
switch (lineJoin)
{
case LineJoin.Round:
shape.StrokeLineJoin = PenLineJoin.Round;
break;
case LineJoin.Bevel:
shape.StrokeLineJoin = PenLineJoin.Bevel;
break;
// The default StrokeLineJoin is Miter
}
shape.StrokeThickness = thickness;
if (dashArray != null)
{
shape.StrokeDashArray = CreateDashArrayCollection(dashArray);
}
if (aliased)
{
// shape.UseLayoutRounding = aliased;
}
}
}
/// <summary>
/// Applies the clip rectangle.
/// </summary>
/// <param name="image">The image.</param>
/// <param name="x">The x offset of the element.</param>
/// <param name="y">The y offset of the element.</param>
private void ApplyClip(UIElement image, double x, double y)
{
image.Clip = new RectangleGeometry { Rect = new Rect(this.clipRect.X - x, this.clipRect.Y - y, this.clipRect.Width, this.clipRect.Height) };
}
/// <summary>
/// Gets the bitmap source.
/// </summary>
/// <param name="image">The image.</param>
/// <returns>The bitmap source.</returns>
private BitmapSource GetImageSource(OxyImage image)
{
if (image == null)
{
return null;
}
if (!this.imagesInUse.Contains(image))
{
this.imagesInUse.Add(image);
}
BitmapSource src;
if (this.imageCache.TryGetValue(image, out src))
{
return src;
}
var bitmapImage = new BitmapImage();
var imageStream = ConvertToRandomAccessStream(image.GetData()).GetAwaiter().GetResult();
bitmapImage.SetSource(imageStream);
this.imageCache.Add(image, bitmapImage);
return bitmapImage;
}
/// <summary>
/// Converts the specified byte array to a <see cref="IRandomAccessStream" />.
/// </summary>
/// <param name="buffer"></param>
/// <returns></returns>
private static async Task<IRandomAccessStream> ConvertToRandomAccessStream(byte[] buffer)
{
//https://stackoverflow.com/questions/16397509/how-to-convert-byte-array-to-inmemoryrandomaccessstream-or-irandomaccessstream-i
var randomAccessStream = new InMemoryRandomAccessStream();
await randomAccessStream.WriteAsync(buffer.AsBuffer());
randomAccessStream.Seek(0);
return randomAccessStream;
}
}
}