/// <summary> /// Draws this Graph onto the specified GDI+ drawing surface. /// </summary> /// <param name="g">The GDI+ drawing surface to draw onto.</param> /// <param name="size">The size of the GDI+ drawing surface.</param> /// <param name="resolution">The resolution to plot with. Coarser resolutions may allow /// the graph to be plotted faster at the expense of visual accuracy, though this is /// dependent on the implementations of the resources on the Graph.</param> public void Draw(Graphics g, Size size, PlotResolution resolution) { g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.SingleBitPerPixelGridFit; g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighSpeed; g.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.HighSpeed; float errorX = 5, errorY = 0; using (Font errorFont = new Font(SystemFonts.MessageBoxFont.FontFamily, 20f, FontStyle.Bold)) { foreach (KeyValuePair <IPlottable, PlottableParameters> plottable in Resources) { string plottableName = plottable.Key is Resource ? (plottable.Key as Resource).Name : plottable.GetType().Name; string error = null; if (plottable.Key.CanPlot(Parameters.HorizontalAxis, Parameters.VerticalAxis)) { try { plottable.Key.PlotOnto(this, g, size, plottable.Value, resolution); } catch (Exception ex) { error = String.Format( "Could not plot \"{0}\": {1} ({2})", plottableName, ex.Message, ex.GetType().Name); } } else { error = String.Format( "Could not plot \"{0}\": it does not contain variables {1} and {2}.", plottableName, Parameters.HorizontalAxis, Parameters.VerticalAxis); } if (error != null) { SizeF errorSize = g.MeasureString(error, errorFont, size.Width); g.DrawString(error, errorFont, Brushes.Red, new RectangleF( errorX, errorY, size.Width - errorY, size.Height - errorX)); errorY += 5 + errorSize.Height; } } } }
/// <summary> /// Plots this equation explicitly on the graph. This is faster than implicit plotting but plotting explicitly with respect /// to a variable (eg. <c>'y'</c>) requires that variable to occur once and once only in an equation, alone on one side of /// the equation. For example, the equation <c>y=x^2-3x+2</c> explicitly defines the value of y, but <c>y^2+x^2=4</c> does not, /// instead using it generally in the terms of the equation. This method plots <paramref name="explicitVariable"/> on the /// vertical axis of <paramref name="graph"/>. /// </summary> /// <param name="graph">The Graph to plot this IPlottable onto.</param> /// <param name="graphics">The GDI+ drawing surface to use for plotting this IPlottable.</param> /// <param name="graphSize">The size of the Graph on the screen. This is a property of the display rather than the /// graph and is thus not included in the graph's parameters.</param> /// <param name="graphPen">The pen to use for drawing the curve of the equation.</param> /// <param name="resolution">The plotting resolution to use. Using a coarser resolution may make the plotting /// process faster, and is thus more suitable when the display is being resized or moved.</param> /// <param name="explicitVariable">The variable that is explicitly defined.</param> /// <param name="node">The parse tree node representing what <paramref name="explicitVariable"/> is explicitly equal to.</param> private void PlotExplicitVertical(Graph graph, Graphics graphics, Size graphSize, Pen graphPen, char explicitVariable, ParseTreeNode node, PlotResolution resolution) { VariableSet vars = new VariableSet(); int previousX = -1, previousY = -1; // use wider intervals for coarser plot resolutions float interval = 0.1f; if (resolution == PlotResolution.Resize) { interval *= 100f; } if (resolution == PlotResolution.Edit) { interval *= 40f; } // evaluate the equation at every point along the axis and plot accordingly for (float i = 0; i < graphSize.Width; i += interval) { double vertical = graph.Parameters.VerticalPixelScale * -(i - graphSize.Height / 2) + graph.Parameters.CenterVertical; vars[explicitVariable] = vertical; double horizontal = node.Evaluate(vars); int x, y; graph.ToImageSpace(graphSize, horizontal, vertical, out x, out y); if (previousX != -1) { if (x < -100 || x >= graphSize.Width + 100) { x = -1; y = -1; } else { try { graphics.DrawLine(graphPen, previousX, previousY, x, y); } catch (OverflowException) // can happen if the graph line shoots up tending to infinity { x = -1; y = -1; } } } previousX = x; previousY = y; } }
/// <summary> /// Plots this Equation onto the specified <paramref name="graph"/> with the given <paramref name="plotParams"/>. /// This method will determine which method of plotting is most appropriate for this equation. This could be the /// implicit plotting method for implicit equations, or explicit plotting with respect to one of the two plotted /// variables in the graph. Explicit plotting will be attempted wherever possible. /// </summary> /// <param name="graph">The Graph to plot this IPlottable onto.</param> /// <param name="graphics">The GDI+ drawing surface to use for plotting this IPlottable.</param> /// <param name="graphSize">The size of the Graph on the screen. This is a property of the display rather than the /// graph and is thus not included in the graph's parameters.</param> /// <param name="plotParams">The parameters used to plot this IPlottable.</param> /// <param name="resolution">The plotting resolution to use. Using a coarser resolution may make the plotting /// process faster, and is thus more suitable when the display is being resized or moved.</param> public void PlotOnto(Graph graph, Graphics graphics, Size graphSize, PlottableParameters plotParams, PlotResolution resolution) { if (resolution == PlotResolution.Resize) { return; } if (ParseTree == null) { Parse(); } BinaryParseTreeNode parseTreeRoot = ParseTree as BinaryParseTreeNode; var originalSmoothingMode = graphics.SmoothingMode; graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias; // smoother pen for the graph using (Pen graphPen = new Pen(plotParams.PlotColor, EquationPenWidth)) { if (parseTreeRoot.Left is VariableParseTreeNode && // if the only instance of a variable is on the left eg. y=x+1 !parseTreeRoot.Right.ContainsVariable((parseTreeRoot.Left as VariableParseTreeNode).Variable)) { PlotExplicit( graph, graphics, graphSize, graphPen, (parseTreeRoot.Left as VariableParseTreeNode).Variable, parseTreeRoot.Right, resolution); } else if (parseTreeRoot.Right is VariableParseTreeNode && // if the only instance of a variable is on the right eg. y-1=x !parseTreeRoot.Left.ContainsVariable((parseTreeRoot.Right as VariableParseTreeNode).Variable)) { PlotExplicit( graph, graphics, graphSize, graphPen, (parseTreeRoot.Right as VariableParseTreeNode).Variable, parseTreeRoot.Left, resolution); } else // if variables are equated implicitly eg. xy=x+y-x^2 { PlotImplicit(graph, graphics, graphSize, graphPen, resolution); } } graphics.SmoothingMode = originalSmoothingMode; }
/// <summary> /// Plots this equation explicitly on the graph. This is faster than implicit plotting but plotting explicitly with respect /// to a variable (eg. <c>'y'</c>) requires that variable to occur once and once only in an equation, alone on one side of /// the equation. For example, the equation <c>y=x^2-3x+2</c> explicitly defines the value of y, but <c>y^2+x^2=4</c> does not, /// instead using it generally in the terms of the equation. This method will determine if the equation is to be plotted on the /// horizontal or vertical axis of <paramref name="graph"/>. /// </summary> /// <param name="graph">The Graph to plot this IPlottable onto.</param> /// <param name="graphics">The GDI+ drawing surface to use for plotting this IPlottable.</param> /// <param name="graphSize">The size of the Graph on the screen. This is a property of the display rather than the /// graph and is thus not included in the graph's parameters.</param> /// <param name="graphPen">The pen to use for drawing the curve of the equation.</param> /// <param name="resolution">The plotting resolution to use. Using a coarser resolution may make the plotting /// process faster, and is thus more suitable when the display is being resized or moved.</param> /// <param name="explicitVariable">The variable that is explicitly defined.</param> /// <param name="node">The parse tree node representing what <paramref name="explicitVariable"/> is explicitly equal to.</param> private void PlotExplicit(Graph graph, Graphics graphics, Size graphSize, Pen graphPen, char explicitVariable, ParseTreeNode node, PlotResolution resolution) { if (explicitVariable == graph.Parameters.HorizontalAxis) { PlotExplicitVertical(graph, graphics, graphSize, graphPen, graph.Parameters.VerticalAxis, node, resolution); // x=... } else if (explicitVariable == graph.Parameters.VerticalAxis) { PlotExplicitHorizontal(graph, graphics, graphSize, graphPen, graph.Parameters.HorizontalAxis, node, resolution); // y=... } else { // actually plotting implicitly in relation to a constant (eg. xy=a) // this functionality isn't yet supported but we're accounting for the // possibility of its support in the future here anyway PlotImplicit(graph, graphics, graphSize, graphPen, resolution); } }
/// <summary> /// Plots this equation implicitly using the marching-squares algorithm and linear interpolation within /// the squares; see the coursework specification of Graphmatic for a better explanation of this algorithm in /// pseudo code. /// </summary> /// <param name="graph">The Graph to plot this IPlottable onto.</param> /// <param name="graphics">The GDI+ drawing surface to use for plotting this IPlottable.</param> /// <param name="graphSize">The size of the Graph on the screen. This is a property of the display rather than the /// graph and is thus not included in the graph's parameters.</param> /// <param name="graphPen">The pen to use for drawing the curve of the equation.</param> /// <param name="resolution">The plotting resolution to use. Using a coarser resolution may make the plotting /// process faster, and is thus more suitable when the display is being resized or moved.</param> private void PlotImplicit(Graph graph, Graphics graphics, Size graphSize, Pen graphPen, PlotResolution resolution) { // use a bigger grid at coarser resolutions int gridResolution = EquationResolution; if (resolution == PlotResolution.Resize) { gridResolution *= 10; } if (resolution == PlotResolution.Edit) { gridResolution *= 4; } VariableSet vars = new VariableSet(); int gridWidth = graphSize.Width / gridResolution, gridHeight = graphSize.Height / gridResolution; // evaluates the Equation parse tree at the corners of each square on a grid on the screen double[,] values = new double[ gridWidth + 1, gridHeight + 1]; double horizontal, vertical; for (int i = 0; i < gridWidth + 1; i++) { for (int j = 0; j < gridHeight + 1; j++) { graph.ToScreenSpace(graphSize, i * gridResolution, j * gridResolution, out horizontal, out vertical); vars[graph.Parameters.HorizontalAxis] = horizontal; vars[graph.Parameters.VerticalAxis] = vertical; values[i, j] = ParseTree.Evaluate(vars); } } // marching squares for (int i = 0; i < gridWidth; i++) { for (int j = 0; j < gridHeight; j++) { // the four values at the corners of this grid cell // the corners are in this layout: // // A---B // | | // | | // C---D double av = values[i, j], bv = values[i + 1, j], cv = values[i, j + 1], dv = values[i + 1, j + 1]; if (Double.IsInfinity(av) || Double.IsNaN(av) || Double.IsInfinity(bv) || Double.IsNaN(bv) || Double.IsInfinity(cv) || Double.IsNaN(cv) || Double.IsInfinity(dv) || Double.IsNaN(dv)) { // don't try to plot these values continue; } // see which edges of the aforementioned grid cell have a change of // sign of the value. For example, ab stores whether the value at A // has a different sign to that at B (using the xor operator). bool ab = ((av > 0) ^ (bv > 0)), bd = ((bv > 0) ^ (dv > 0)), ac = ((av > 0) ^ (cv > 0)), cd = ((cv > 0) ^ (dv > 0)); int x = i * gridResolution, y = j * gridResolution; // guess the points on the 4 edges at which the sign change occurs // this is a linear interpolation and thus isn't perfect but it's good enough // and is almost perfect at low enough grid resolutions. If a sign change does // not occur, this point will not actually be within the boundaries of the edge // but that point won't be used anyway so it doesn't matter Point abp = new Point(x + ZeroReverseLerp(gridResolution, av, bv), y), bdp = new Point(x + gridResolution, y + ZeroReverseLerp(gridResolution, bv, dv)), acp = new Point(x, y + ZeroReverseLerp(gridResolution, av, cv)), cdp = new Point(x + ZeroReverseLerp(gridResolution, cv, dv), y + gridResolution); // plot lines between the appropriate points where the sign changes if (ab && bd && ac && cd) { ; // if the sign changes on all 4 edges, don't even bother plotting lines as it's an ambiguous case } else if (!ab && !bd && !ac && !cd) { ; // the sign doesn't change at all, so still do nothing } else if (ac && cd) { graphics.DrawLine(graphPen, acp, cdp); } else if (bd && cd) { graphics.DrawLine(graphPen, bdp, cdp); } else if (bd && ac) { graphics.DrawLine(graphPen, bdp, acp); } else if (ab && bd) { graphics.DrawLine(graphPen, abp, bdp); } else if (ab && cd) { graphics.DrawLine(graphPen, abp, cdp); } else if (ab && ac) { graphics.DrawLine(graphPen, abp, acp); } // the sign will always change 0, 2 or 4 times in a cell } } }
/// <summary> /// Plots this DataSet onto <paramref name="graph"/>. /// </summary> /// <param name="graph">The Graph to plot this IPlottable onto.</param> /// <param name="graphics">The GDI+ drawing surface to use for plotting this IPlottable.</param> /// <param name="graphSize">The size of the Graph on the screen. This is a property of the display rather than the /// graph and is thus not included in the graph's parameters.</param> /// <param name="plotParams">The parameters used to plot this IPlottable.</param> /// <param name="resolution">The plotting resolution to use. This does not have an effect for data sets.</param> public void PlotOnto(Graph graph, Graphics graphics, Size graphSize, PlottableParameters plotParams, PlotResolution resolution) { if (resolution == PlotResolution.Resize) { return; } int horizontalVariableIndex = IndexOfVariable(graph.Parameters.HorizontalAxis), verticalVariableIndex = IndexOfVariable(graph.Parameters.VerticalAxis); if (horizontalVariableIndex == -1) { throw new Exception("This data set cannot be plotted over the " + graph.Parameters.HorizontalAxis.ToString() + " variable, as it does not contain such a variable."); } if (verticalVariableIndex == -1) { throw new Exception("This data set cannot be plotted over the " + graph.Parameters.VerticalAxis.ToString() + " variable, as it does not contain such a variable."); } using (Pen dataPointPen = new Pen(plotParams.PlotColor, DataPointPenWidth)) { foreach (double[] row in Data) { double horizontal = row[horizontalVariableIndex], vertical = row[verticalVariableIndex]; int graphX, graphY; graph.ToImageSpace( graphSize, horizontal, vertical, out graphX, out graphY); graphics.DrawLine(dataPointPen, graphX - DataPointCrossSize, graphY - DataPointCrossSize, graphX + DataPointCrossSize, graphY + DataPointCrossSize); graphics.DrawLine(dataPointPen, graphX - DataPointCrossSize, graphY + DataPointCrossSize, graphX + DataPointCrossSize, graphY - DataPointCrossSize); } } }
/// <summary> /// Draws this Annotation onto <paramref name="page"/>. /// </summary> /// <param name="page">The Graph to plot this Annotation onto.</param> /// <param name="graphics">The GDI+ drawing surface to use for plotting this Annotation.</param> /// <param name="graphSize">The size of the Graph on the screen. This is a property of the display rather than the /// graph and is thus not included in the page's graph's parameters.</param> /// <param name="resolution">The plotting resolution to use. This does not have an effect for data sets.</param> public override void DrawAnnotationOnto(Page page, Graphics graphics, Size graphSize, PlotResolution resolution) { Point[] screenPoints = Points .Select(p => { int x, y; page.Graph.ToImageSpace( graphSize, p.Item1 * Width + X, p.Item2 * Height + Y, out x, out y); return(new Point(x, y)); }) .ToArray(); using (Pen annotationPen = new Pen(Color, Thickness)) { annotationPen.LineJoin = System.Drawing.Drawing2D.LineJoin.Round; if (Type == DrawingType.Highlight) { annotationPen.EndCap = annotationPen.StartCap = System.Drawing.Drawing2D.LineCap.Flat; } else { annotationPen.EndCap = annotationPen.StartCap = System.Drawing.Drawing2D.LineCap.Round; } graphics.DrawLines(annotationPen, screenPoints); } }
/// <summary> /// Draws the selection indicator around this Drawing onto <paramref name="page"/>.<para/> /// This draws red points on each point in this Drawing's path. /// </summary> /// <param name="page">The Page to plot this Annotation onto.</param> /// <param name="graphics">The GDI+ drawing surface to use for plotting this Annotation.</param> /// <param name="graphSize">The size of the Graph on the screen. This is a property of the display rather than the /// graph and is thus not included in the page's graph's parameters.</param> /// <param name="resolution">The plotting resolution to use. This does not have an effect for data sets.</param> public override void DrawSelectionIndicatorOnto(Page page, Graphics graphics, Size graphSize, PlotResolution resolution) { Point[] screenPoints = Points .Select(p => { int x, y; page.Graph.ToImageSpace( graphSize, p.Item1 * Width + X, p.Item2 * Height + Y, out x, out y); return(new Point(x, y)); }) .ToArray(); using (Brush annotationBrush = new SolidBrush(Color.Red)) { foreach (var screenPoint in screenPoints) { graphics.FillEllipse(annotationBrush, screenPoint.X - (int)(Thickness * 0.65), screenPoint.Y - (int)(Thickness * 0.65), (int)(Thickness * 1.3), (int)(Thickness * 1.3)); } } base.DrawSelectionIndicatorOnto(page, graphics, graphSize, resolution); }
/// <summary> /// Plots this IPlottable onto a given graph. The specific method of plotting will depend on the implementation of the /// IPlottable. /// </summary> /// <param name="graph">The Graph to plot this IPlottable onto.</param> /// <param name="graphics">The GDI+ drawing surface to use for plotting this IPlottable.</param> /// <param name="graphSize">The size of the Graph on the screen. This is a property of the display rather than the /// graph and is thus not included in the graph's parameters.</param> /// <param name="plotParams">The parameters used to plot this IPlottable.</param> /// <param name="resolution">The plotting resolution to use. Using a coarser resolution may make the plotting /// process faster, and is thus more suitable when the display is being resized or moved.</param> public void PlotOnto(Graph graph, Graphics graphics, Size graphSize, PlottableParameters plotParams, PlotResolution resolution) { using (Brush brush = new SolidBrush(plotParams.PlotColor)) { float currentY = graphSize.Height; int offset = 5; foreach (IPlottable plot in graph.Reverse()) { if (plot is Resource) // only plot Resources, so we don't plot the key on the key { PlottableParameters parameters = graph[plot]; Resource resource = plot as Resource; string name = resource.Name; SizeF textSize = graphics.MeasureString(name, SystemFonts.DefaultFont); currentY -= textSize.Height + offset; float resourceX = graphSize.Width - offset - textSize.Width; graphics.DrawString(name, SystemFonts.DefaultFont, brush, resourceX, currentY); using (Pen resourcePen = new Pen(parameters.PlotColor)) // plot in the color of the resource { if (resource is DataSet) { // draw a cross for a data set resourcePen.Width = DataSet.DataPointPenWidth; graphics.DrawLine(resourcePen, new PointF(resourceX - DataSet.DataPointCrossSize - 5, currentY - DataSet.DataPointCrossSize + textSize.Height / 2), new PointF(resourceX + DataSet.DataPointCrossSize - 5, currentY + DataSet.DataPointCrossSize + textSize.Height / 2)); graphics.DrawLine(resourcePen, new PointF(resourceX - DataSet.DataPointCrossSize - 5, currentY + DataSet.DataPointCrossSize + textSize.Height / 2), new PointF(resourceX + DataSet.DataPointCrossSize - 5, currentY - DataSet.DataPointCrossSize + textSize.Height / 2)); } else if (resource is Equation) { // draw a line for an equation resourcePen.Width = Equation.EquationPenWidth; graphics.DrawLine(resourcePen, new PointF(resourceX - 2, currentY + textSize.Height / 2), new PointF(resourceX - 15, currentY + textSize.Height / 2)); } } } } } }
/// <summary> /// Draws the selection indicator around this Annotation onto <paramref name="page"/>. /// </summary> /// <param name="page">The Page to plot this Annotation onto.</param> /// <param name="graphics">The GDI+ drawing surface to use for plotting this Annotation.</param> /// <param name="graphSize">The size of the Graph on the screen. This is a property of the display rather than the /// graph and is thus not included in the page's graph's parameters.</param> /// <param name="resolution">The plotting resolution to use. This does not have an effect for data sets.</param> public virtual void DrawSelectionIndicatorOnto(Page page, Graphics graphics, Size graphSize, PlotResolution resolution) { Rectangle screenRectangle = GetScreenRectangle(page, graphSize); using (Pen selectBoxPen = new Pen(Color.Red, 2f)) { Brush resizeNodeBrush = Brushes.Yellow; selectBoxPen.DashStyle = System.Drawing.Drawing2D.DashStyle.Dot; graphics.DrawRectangle(selectBoxPen, screenRectangle); graphics.FillEllipse(resizeNodeBrush, screenRectangle.X + screenRectangle.Width - 4, screenRectangle.Y - 4, 9, 9); graphics.DrawEllipse(selectBoxPen, screenRectangle.X + screenRectangle.Width - 4, screenRectangle.Y - 4, 9, 9); } }
/// <summary> /// Draws this Annotation onto <paramref name="page"/>. /// </summary> /// <param name="page">The Graph to plot this Annotation onto.</param> /// <param name="graphics">The GDI+ drawing surface to use for plotting this Annotation.</param> /// <param name="graphSize">The size of the Graph on the screen. This is a property of the display rather than the /// graph and is thus not included in the page's graph's parameters.</param> /// <param name="resolution">The plotting resolution to use. This does not have an effect for data sets.</param> public virtual void DrawAnnotationOnto(Page page, Graphics graphics, Size graphSize, PlotResolution resolution) { Rectangle screenRectangle = GetScreenRectangle(page, graphSize); using (Brush annotationBrush = new SolidBrush(Color)) { graphics.FillRectangle(annotationBrush, screenRectangle); } }
/// <summary> /// Draws this Annotation onto <paramref name="page"/>. /// </summary> /// <param name="page">The Graph to plot this Annotation onto.</param> /// <param name="graphics">The GDI+ drawing surface to use for plotting this Annotation.</param> /// <param name="graphSize">The size of the Graph on the screen. This is a property of the display rather than the /// graph and is thus not included in the page's graph's parameters.</param> /// <param name="resolution">The plotting resolution to use. This does not have an effect for data sets.</param> public override void DrawAnnotationOnto(Page page, Graphics graphics, Size graphSize, PlotResolution resolution) { graphics.DrawImage(ImageData, GetScreenRectangle(page, graphSize)); }
/// <summary> /// Plots this IPlottable onto a given graph. The specific method of plotting will depend on the implementation of the /// IPlottable. /// </summary> /// <param name="graph">The Graph to plot this IPlottable onto.</param> /// <param name="graphics">The GDI+ drawing surface to use for plotting this IPlottable.</param> /// <param name="graphSize">The size of the Graph on the screen. This is a property of the display rather than the /// graph and is thus not included in the graph's parameters.</param> /// <param name="plotParams">The parameters used to plot this IPlottable.</param> /// <param name="resolution">The plotting resolution to use. Using a coarser resolution may make the plotting /// process faster, and is thus more suitable when the display is being resized or moved.</param> public void PlotOnto(Graph graph, Graphics g, Size graphSize, PlottableParameters plotParams, PlotResolution resolution) { int originX, originY; graph.ToImageSpace(graphSize, 0, 0, out originX, out originY); PlotGridLinesOnto(graph, g, graphSize, plotParams, originX, originY); PlotAxesOnto(g, graphSize, plotParams, originX, originY); }