/* Copyright (c) Microsoft Corporation All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT. See the Apache Version 2.0 License for specific language governing permissions and limitations under the License. */ namespace Microsoft.Research.Calypso.Tools { using System; using System.Drawing; using System.Drawing.Drawing2D; using System.Windows.Forms; /// /// A 2D drawing surface with FP coordinates. /// public class DrawingSurface2D { /// /// Panel where the drawing is done. /// Panel drawingpanel; /// /// Graphics context used for drawing. /// Graphics grph; /// /// Screen graphics context (used for some transient pictures). /// Graphics screenGrph; /// /// Margins around the axes of the drawing area, in pixels. /// public int rightMargin, leftMargin; // public because other areas need to know about them /// /// Drawing area border coordinates, must be set before drawing anything. /// Rectangle2D boundingBox; /// /// Double-buffering support; by setting this to null in the constructor you can disable double-buffering. /// Bitmap buffer; /// /// Old size of canvas, before resizing. /// Size oldsize; /// /// Width of drawing area on screen in pixels, excluding margins. /// private int drawingAreaWidth; /// /// Height of drawing area on screen in pixels, excluding margins. /// private int drawingAreaHeight; /// /// Scaling coefficient on x (from drawing to screen coordinates). /// private double xScale; /// /// Scaling coefficient on y (from drawing to screen coordinates). /// private double yScale; /// /// Width of the drawing area. /// public double Width { get; protected set; } /// /// Height of the drawing area. /// public double Height { get; protected set; } /// /// Instantiate a 2D drawing surface. /// /// Panel where the image ends up being drawn. public DrawingSurface2D(Panel panel) { // Comment-out the assignment to buffer to disable double-buffering. buffer = new Bitmap(panel.Width, panel.Height); this.drawingpanel = panel; screenGrph = this.drawingpanel.CreateGraphics(); if (buffer != null) grph = Graphics.FromImage(buffer); else grph = screenGrph; this.oldsize = panel.Size; grph.SmoothingMode = SmoothingMode.AntiAlias; this.RecomputeCachedValues(); } /// /// Things may have changed, recompute some cached values. /// private void RecomputeCachedValues() { this.drawingAreaWidth = this.drawingpanel.Size.Width - this.leftMargin - this.rightMargin; this.drawingAreaHeight = this.drawingpanel.Size.Height - this.TopMargin - this.BottomMargin; if (this.boundingBox != null) { this.Width = this.xDrawMax - this.xDrawMin; this.Height = this.yDrawMax - this.yDrawMin; this.yScale = this.drawingAreaHeight / this.Height; this.xScale = this.drawingAreaWidth / this.Width; } } /// /// Set the margins in pixels around the area which is mapped to. /// /// Distance in pixels from top of panel to top of drawing area. /// Distance in pixels from bottom of panel to bottom of drawing area. /// Distance in pixels from right of panel to right of drawing area. /// Distance in pixels from left of panel to left of drawing area. public void SetMargins(int top, int bottom, int right, int left) { this.TopMargin = top; this.BottomMargin = bottom; this.rightMargin = right; this.leftMargin = left; this.RecomputeCachedValues(); } private bool fastDrawing; /// /// If true try to draw faster. /// public bool FastDrawing { get { return this.fastDrawing; } set { this.fastDrawing = value; if (!value) this.grph.SmoothingMode = SmoothingMode.AntiAlias; else this.grph.SmoothingMode = SmoothingMode.HighSpeed; } } /// /// How big is the rectangle containing a string in this window? /// /// String to measure. /// Font to draw the text in. /// The size of a rectangle in pixels. public Size MeasureString(string text, Font font) { return this.grph.MeasureString(text, font).ToSize(); } /// /// The bounding box of the drawing area. /// public Rectangle2D BoundingBox { get { return this.boundingBox; } } /// /// The size of the drawing area in pixels. /// /// Size of the drawing area for plots (excluding margins). public Size DrawingAreaSize() { return new Size(this.drawingAreaWidth, this.drawingAreaHeight); } /// /// Maximum X coordinate of drawing area. /// public double xDrawMax { get { return boundingBox.Corner2.X; } } /// /// Maximum Y coordinate of drawing area. /// public double yDrawMax { get { return boundingBox.Corner2.Y; } } /// /// Minimum X coordinate of drawing area. /// public double xDrawMin { get { return boundingBox.Corner1.X; } } /// /// Minimum Y coordinate of drawing area. /// public double yDrawMin { get { return boundingBox.Corner1.Y; } } /// /// Number of pixels above graphics plot. /// public int TopMargin { get; protected set; } /// /// Number of pixels below graphics plot. /// public int BottomMargin { get; protected set; } /// /// Size in pixels of the drawing panel including all margins. /// public Size SurfaceSize { get { return this.drawingpanel.Size; } } /// /// Before drawing anything one has to set the drawing area size. /// This maps the four specified coordinates to the corners of the drawing panel. /// /// Perimeter of area. public void SetDrawingArea(Rectangle2D bbox) { if (bbox.Degenerate()) throw new ArgumentException("Cannot draw in a degenerate bounding box"); this.boundingBox = bbox; this.RecomputeCachedValues(); } /// /// Scale one point position and compute actual panel pixel coordinates. /// /// X coordinate to scale relative to the drawing area. /// Y coordinate to scale relative to the drawing area. /// Resulting X coordinate after scaling (in panel coordinates). /// Resulting Y coordinate after scaling (in panel coordinates). public void Scale(double x, double y, out int xo, out int yo) { double ynew = (y - this.yDrawMin) * this.yScale; double xnew = (x - this.xDrawMin) * this.xScale; xo = this.leftMargin + (int)xnew; yo = this.TopMargin + this.drawingAreaHeight - (int)ynew; } /// /// Scale one point position and compute actual panel pixel coordinates. /// /// Point to scale. /// Resulting X coordinate after scaling (in panel coordinates). /// Resulting Y coordinate after scaling (in panel coordinates). public void Scale(Point2D point, out int xo, out int yo) { this.Scale(point.X, point.Y, out xo, out yo); } /// /// Scale one point position and compute actual panel pixel coordinates. /// /// Point to scale. /// Point containing pixel coordinates. public Point Scale(Point2D point) { int xo, yo; this.Scale(point.X, point.Y, out xo, out yo); return new Point(xo, yo); } /// /// Convert a line length on the screen to user coordinates. /// /// Line length. /// The plotting coordinates. public Point2D LengthScreenToUser(Point line) { Point2D res = new Point2D(line.X / this.xScale, line.Y / this.yScale); return res; } /// /// Invert the scaling operation: convert from screen coordinates to plotting coordinates. /// /// X coordinate in pixels in window. /// Y coordinate in pixels in window. /// Original x coordinate, in drawing area. /// Original y coordinate, in drawing area. public void ScreenToUser(int x, int y, out double xo, out double yo) { // remove margins and reflect x -= this.leftMargin; y = this.TopMargin + this.drawingAreaHeight - y; yo = y / this.yScale + this.yDrawMin; xo = x / this.xScale + this.xDrawMin; } /// /// Convert a dimension in screen coordinates to a dimension in plotting coordinates. /// /// Size to convert. /// The result is really a size, encoded as a Point2D. public Point2D Unscale(Point2D size) { return new Point2D(size.X / this.xScale, size.Y / this.yScale); } /// /// The graphics context used for drawing. /// /// If true returns the screen graphics context, else the backing buffer. /// The graphics context used for drawing. internal Graphics GetGraphics(bool onscreen) { return onscreen ? this.screenGrph : this.grph; } /// /// Draw a line in screen coordinates. /// /// Line color. /// Left endpoint. /// Right endpoint. /// Line width. /// Cap line with arrow? /// Draw the line on the screen? public void DrawLine(Color c, Point left, Point right, int lineWidth, bool arrow, bool onscreen) { System.Drawing.Pen linePen = new System.Drawing.Pen(c); linePen.Width = lineWidth; if (arrow) { linePen.EndCap = LineCap.ArrowAnchor; } this.DrawLine(linePen, left, right, onscreen); } /// /// Draw a line in screen coordinates. /// /// Pen used to draw the line. /// Left endpoint. /// Right endpoint. /// Draw the line on the screen? public void DrawLine(Pen pen, Point left, Point right, bool onscreen) { Graphics g = this.GetGraphics(onscreen); g.DrawLine(pen, left, right); } /// /// Draw a line in draw coordinates between two given endpoints. /// /// Pen to use. /// Left endpoint, relative to the DrawingArea. /// Right endpoint, relative to the DrawingArea. /// Should the drawing be made just on screen (transient), /// or on the backing storage (persistent)? /// True if line is visible. public void DrawLine(Pen pen, Point2D left, Point2D right, bool onscreen) { if (!Visible(left, right)) return; Point l = Scale(left); Point r = Scale(right); this.DrawLine(pen, l, r, onscreen); return; } /// /// Draw a rectangle in screen coordinates. /// Left corner. /// Right corner. /// Pen to draw. /// public void DrawRectangle(Point left, Point right, Pen pen) { this.grph.DrawRectangle(pen, left.X, left.Y, right.X - left.X, right.Y - left.Y); } /// /// Check if a point is visible. /// /// Point x coordinate (unscaled). /// Point y coordinate (unscaled). /// True if the point is visible. public bool Visible(double x, double y) { if (x < this.xDrawMin || x > this.xDrawMax) return false; if (y < this.yDrawMin || y > this.yDrawMax) return false; return true; } /// /// True if a point is visible on canvas. /// /// Point to test. /// True if point is visible. public bool Visible(Point2D p) { return this.Visible(p.X, p.Y); } /// /// Is any of these two points visible? /// /// Left endpoint. /// Right endpoint. /// True if any endpoint is on the screen. private bool Visible(Point2D left, Point2D right) { return this.Visible(left) || this.Visible(right); } /// /// Draw a filled rectangle with the given color in the drawing area. /// /// Brush to use for filling. /// Rectangle, relative to the DrawingArea. public void FillRectangle(Brush b, Rectangle2D rect) { Rectangle scaledRect = this.Scale(rect); this.grph.FillRectangle(b, scaledRect); } /// /// Draw a rectangle with the given color in the drawing area. /// /// Rectangle, relative to the DrawingArea. /// Pen used to draw rectangle. public void DrawRectangle(Rectangle2D rect, Pen pen) { Rectangle scaledRect = this.Scale(rect); this.grph.DrawRectangle(pen, scaledRect); } /// /// Draw a rectangle filled with a specific brush. /// /// Brush to use to fill. /// Rectangle to draw. public void DrawRectangle(Brush b, Rectangle2D rect) { Rectangle scaledRect = this.Scale(rect); this.grph.FillRectangle(b, scaledRect); } /// /// Draw a rectangle with the given color in the drawing area. /// /// Color to use. /// Polygon vertices relative to the DrawingArea. /// Fill the polygon with the given color. /// Pen used to draw polygon. public void DrawPolygon(Color c, Point2D [] vertices, Pen pen, bool filled) { Point[] points = new Point[vertices.Length]; for (int i = 0; i < vertices.Length; i++) { points[i] = this.Scale(vertices[i]); } if (filled) { Brush b = new System.Drawing.SolidBrush(c); this.grph.FillPolygon(b, points); } else { this.grph.DrawPolygon(pen, points); } } /// /// Write some text in the drawing area, in absolute coordinates. /// /// Text color. /// Text string. /// Font used to draw text. /// Upper-left corner x coordinate, in pixels. /// Upper-left corner y coordinate, in pixels. public void DrawTextAbsoluteCoordinates(Color c, Font font, string text, int x, int y) { this.grph.DrawString(text, font, new System.Drawing.SolidBrush(c), x, y); } /// /// Draw text rotated with 90 degrees. /// /// Text color. /// Text to draw. /// Font to use for text. /// X coordinate. /// Y coordinate. public void DrawRotatedTextAbsoluteCoordinates(Color c, string text, Font font, int x, int y) { // bring the origin in the center this.grph.TranslateTransform(x, y); this.grph.RotateTransform(-90); this.grph.TranslateTransform(-x, -y); this.grph.DrawString(text, font, new System.Drawing.SolidBrush(c), x, y); this.grph.ResetTransform(); } /// /// Draw text rotated with 90 degrees, right-justified. /// /// Text to draw. /// X coordinate. /// Y coordinate. /// Brush to use for text. /// Font to use for text. /// Angle of rotation in degrees. public void DrawRotatedRightJustifiedTextAbsoluteCoordinates(string text, Font font, Brush brush, int x, int y, int angleDegrees) { // bring the origin in the center this.grph.TranslateTransform(x, y); this.grph.RotateTransform(angleDegrees); this.grph.TranslateTransform(-x, -y); Size textsize = this.MeasureString(text, font); Rectangle box = new Rectangle(x - textsize.Width, y, textsize.Width + 2, textsize.Height); this.grph.DrawString(text, font, brush, box, new StringFormat()); this.grph.ResetTransform(); } /// /// Write some text in the drawing area, in absolute coordinates. /// /// Text to write. /// Brush to use for text. /// Font to use for text. /// Upper-right corner x coordinate, in pixels. /// y coordinate of center, in pixels. public void DrawRightJustifiedTextAbsoluteCoordinates(string text, Font font, Brush brush, int x, int y) { Size textsize = this.MeasureString(text, font); Rectangle box = new Rectangle(x - textsize.Width, y - textsize.Height / 2, textsize.Width + 2, textsize.Height); this.grph.DrawString(text, font, brush, box, new StringFormat()); } /// /// Chose an appropriate font to fit the text in a given box. /// /// Graphics context where the string will be drawn. /// Font to enlarge/shrink. /// How much space is available for the string, in pixels. /// String to write. /// A suitably-sized font. public static Font ChooseFont(Graphics graph, Font baseFont, Size spaceAvailable, string text) { SizeF size = graph.MeasureString(text, baseFont); double scaling = Math.Min(spaceAvailable.Width / size.Width, spaceAvailable.Height / size.Height); double newSize = baseFont.Size * scaling; if (newSize < 2) // too small return null; Font retval = new Font(baseFont.FontFamily, (float)newSize, baseFont.Style); return retval; } /// /// Draw a one-line string to fit in the specified rectangle. /// /// Text to draw. /// Start from this font, but resize it. /// Brush to use for drawing. /// Where to draw it. public void DrawTextInRectangle(string text, Brush brush, Font baseFont, Rectangle2D place) { Rectangle dest = this.Scale(place); Font font = DrawingSurface2D.ChooseFont(this.grph, baseFont, dest.Size, text); if (font == null || font.Size < 3) // too small anyway return; SizeF actualSize = this.grph.MeasureString(text, font); int adjustX = (int)(dest.Width - actualSize.Width) / 2; int adjustY = dest.Height / 2; this.DrawRightJustifiedTextAbsoluteCoordinates(text, font, brush, dest.Right - adjustX, dest.Top + adjustY); } /// /// Write some text in the drawing area. Font is not scaled. /// /// Text to write. /// Brush to use for text. /// Font to use for text. /// Upper-right corner x coordinate. /// Upper-right corner y coordinate. public void DrawRightJustifiedText(string text, Font font, Brush brush, double x, double y) { int xo, yo; this.Scale(x, y, out xo, out yo); Size textsize = this.MeasureString(text, font); Rectangle box = new Rectangle(xo - textsize.Width, yo, textsize.Width + 2, textsize.Height); this.grph.DrawString(text, font, brush, box, new StringFormat()); } /// /// Draw a point in screen pixel coordinates (really a small circle). /// /// Brush used to draw the point. /// Point x coordinate. /// Point y coordinate. /// Size of dot to draw. /// If true draw the point directly on the screen, else on the backing buffer. public void DrawPointAbsoluteCoordinates(Brush brush, int x, int y, int dotSize, bool onscreen) { Graphics g = this.GetGraphics(onscreen); g.FillEllipse(brush, x - dotSize / 2, y - dotSize / 2, dotSize, dotSize); } /// /// Draw a "fat" point in the drawing area. /// /// Brush used to draw point. /// Point x coordinate. /// Point y coordinate. /// Size of dot to draw. /// If true draw the point directly on screen, otherwise to the backing buffer. /// True if point is visible. public bool DrawPoint(Brush brush, double x, double y, int dotSize, bool onscreen) { if (!this.Visible(x, y)) return false; int x1, y1; this.Scale(x, y, out x1, out y1); this.DrawPointAbsoluteCoordinates(brush, x1, y1, dotSize, onscreen); return true; } /// /// Clear the drawing area. /// public void Clear() { this.grph.Clear(drawingpanel.BackColor); } /// /// Invoked when the panel has been resized. /// True if the size has changed and is not zero, false otherwise. /// public bool Resize() { if (this.oldsize == this.drawingpanel.Size) return false; if (this.drawingpanel.Size.Width == 0 && this.drawingpanel.Size.Height == 0) // window has been minimized; do nothing return false; this.oldsize = this.drawingpanel.Size; this.RecomputeCachedValues(); if (this.screenGrph != null) this.screenGrph.Dispose(); this.screenGrph = this.drawingpanel.CreateGraphics(); if (this.buffer != null) { if (this.grph != null) this.grph.Dispose(); if (this.drawingpanel.Width != 0 && this.drawingpanel.Height != 0) { // when minimizing the window the size of the panel can be 0 this.buffer.Dispose(); this.buffer = new Bitmap(this.drawingpanel.Width, this.drawingpanel.Height); this.grph = Graphics.FromImage(buffer); } } else this.grph = this.screenGrph; this.grph.SmoothingMode = SmoothingMode.AntiAlias; return true; } /// /// Paint the graphics area from the bitmap. /// public void Repaint(PaintEventArgs e) { e.Graphics.DrawImageUnscaled(buffer, 0, 0); } /// /// Scale and normalize a rectangle. /// /// Rectangle to normalize. /// Corresponding rectangle in pixels. private Rectangle Scale(Rectangle2D rectangle) { int xl1, yl1, xr1, yr1; this.Scale(rectangle.Corner1, out xl1, out yl1); this.Scale(rectangle.Corner2, out xr1, out yr1); int deltax = xr1 - xl1; int deltay = yr1 - yl1; if (deltax < 0) { xl1 += deltax; deltax = -deltax; } if (deltax == 0) { // zero is not shown, make it at least 1. deltax = 1; } if (deltay < 0) { yl1 += deltay; deltay = -deltay; } if (deltay == 0) { // zero is not shown, make it at least 1. deltay = 1; } Rectangle retval = new Rectangle(new Point(xl1, yl1), new Size(deltax, deltay)); return retval; } /// /// Draw an ellipse. /// /// Fill color. /// Rectangle enclosing ellipse. /// Pen to use. /// If true, fill the ellipse. internal void DrawEllipse(Color color, Rectangle2D rect, Pen pen, bool filled) { Rectangle scaledRect = this.Scale(rect); if (filled) { Brush b = new System.Drawing.SolidBrush(color); this.grph.FillEllipse(b, scaledRect); } else { this.grph.DrawEllipse(pen, scaledRect); } } /// /// The image plotted. /// /// A bitmap. public Bitmap PlottedImage() { return this.buffer; } } }