// -------------------------------------------------------------------------------------------------------------------- // // Copyright (c) 2014 OxyPlot contributors // // // Implements for . // // -------------------------------------------------------------------------------------------------------------------- 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; /// /// Implements for . /// public class RenderContext : IRenderContext { /// /// The brush cache. /// private readonly Dictionary brushCache = new Dictionary(); /// /// The canvas. /// private readonly Canvas canvas; /// /// The images in use /// private readonly HashSet imagesInUse = new HashSet(); /// /// The image cache /// private readonly Dictionary imageCache = new Dictionary(); /// /// The current tool tip /// private string currentToolTip; /// /// The clip rectangle. /// private Rect clipRect; /// /// The clip flag. /// private bool clip; /// /// Initializes a new instance of the class. /// /// The canvas. public RenderContext(Canvas canvas) { this.canvas = canvas; this.Width = canvas.ActualWidth; this.Height = canvas.ActualHeight; this.RendersToScreen = true; } /// /// Gets the height. /// /// The height. public double Height { get; private set; } /// /// Gets a value indicating whether to paint the background. /// /// true if the background should be painted; otherwise, false. public bool PaintBackground { get { return false; } } /// /// Gets the width. /// /// The width. public double Width { get; private set; } /// /// Gets or sets a value indicating whether the context renders to screen. /// /// true if the context renders to screen; otherwise, false. public bool RendersToScreen { get; set; } /// /// Draws an ellipse. /// /// The rectangle. /// The fill color. /// The stroke color. /// The thickness. 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); } /// /// Draws the collection of ellipses, where all have the same stroke and fill. /// This performs better than calling DrawEllipse multiple times. /// /// The rectangles. /// The fill color. /// The stroke color. /// The stroke thickness. public void DrawEllipses(IList 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); } /// /// Draws the polyline from the specified points. /// /// The points. /// The stroke color. /// The stroke thickness. /// The dash array. /// The line join type. /// if set to true the shape will be aliased. public void DrawLine( IList 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); } /// /// 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. /// /// The points. /// The stroke color. /// The stroke thickness. /// The dash array. /// The line join type. /// if set to true the shape will be aliased. public void DrawLineSegments( IList 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); } /// /// Draws the polygon from the specified points. The polygon can have stroke and/or fill. /// /// The points. /// The fill color. /// The stroke color. /// The stroke thickness. /// The dash array. /// The line join type. /// if set to true the shape will be aliased. public void DrawPolygon( IList 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); } /// /// Draws a collection of polygons, where all polygons have the same stroke and fill. /// This performs better than calling DrawPolygon multiple times. /// /// The polygons. /// The fill color. /// The stroke color. /// The stroke thickness. /// The dash array. /// The line join type. /// if set to true the shape will be aliased. public void DrawPolygons( IList> 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); } /// /// Draws the rectangle. /// /// The rectangle. /// The fill color. /// The stroke color. /// The stroke thickness. 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); } /// /// Draws a collection of rectangles, where all have the same stroke and fill. /// This performs better than calling DrawRectangle multiple times. /// /// The rectangles. /// The fill color. /// The stroke color. /// The stroke thickness. public void DrawRectangles(IList 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); } /// /// Draws the text. /// /// The position. /// The text. /// The fill color. /// The font family. /// Size of the font. /// The font weight. /// The rotation angle. /// The horizontal alignment. /// The vertical alignment. /// The maximum size of the text. 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); } } /// /// Measures the text. /// /// The text. /// The font family. /// Size of the font. /// The font weight. /// The text size. 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); } /// /// Sets the tool tip for the following items. /// /// The text in the tooltip. public void SetToolTip(string text) { this.currentToolTip = text; } /// /// Draws the specified portion of the specified at the specified location and with the specified size. /// /// The source. /// The x-coordinate of the upper-left corner of the portion of the source image to draw. /// The y-coordinate of the upper-left corner of the portion of the source image to draw. /// Width of the portion of the source image to draw. /// Height of the portion of the source image to draw. /// The x-coordinate of the upper-left corner of drawn image. /// The y-coordinate of the upper-left corner of drawn image. /// The width of the drawn image. /// The height of the drawn image. /// The opacity. /// interpolate if set to true. 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); } /// /// Sets the clipping rectangle. /// /// The clipping rectangle. /// True if the clipping rectangle was set. public bool SetClip(OxyRect clippingRect) { this.clipRect = clippingRect.ToRect(false); this.clip = true; return true; } /// /// Resets the clipping rectangle. /// public void ResetClip() { this.clip = false; } /// /// Cleans up resources not in use. /// /// This method is called at the end of each rendering. 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(); } /// /// Creates the dash array collection. /// /// The dash array. /// A DoubleCollection. private static DoubleCollection CreateDashArrayCollection(IEnumerable dashArray) { var dac = new DoubleCollection(); foreach (double v in dashArray) { dac.Add(v); } return dac; } /// /// Gets the font weight. /// /// The font weight. /// A private static FontWeight GetFontWeight(double fontWeight) { return fontWeight > OxyPlot.FontWeights.Normal ? FontWeights.Bold : FontWeights.Normal; } /// /// Adds the specified element to the canvas. /// /// The element. /// The clip offset X. /// The clip offset Y. private void Add(UIElement element, double clipOffsetX = 0, double clipOffsetY = 0) { if (this.clip) { this.ApplyClip(element, clipOffsetX, clipOffsetY); } this.canvas.Children.Add(element); } /// /// Applies the tooltip to the specified element. /// /// The element. private void ApplyTooltip(DependencyObject element) { if (!string.IsNullOrEmpty(this.currentToolTip)) { ToolTipService.SetToolTip(element, this.currentToolTip); } } /// /// Gets the cached brush. /// /// The stroke. /// The brush. 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; } /// /// Sets the stroke of the specified shape. /// /// The shape. /// The stroke. /// The thickness. /// The line join. /// The dash array. /// aliased if set to true. private void SetStroke( Shape shape, OxyColor stroke, double thickness, LineJoin lineJoin = LineJoin.Miter, IEnumerable 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; } } } /// /// Applies the clip rectangle. /// /// The image. /// The x offset of the element. /// The y offset of the element. 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) }; } /// /// Gets the bitmap source. /// /// The image. /// The bitmap source. 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; } /// /// Converts the specified byte array to a . /// /// /// private static async Task 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; } } }