Satsuma
a delicious .NET graph library
 All Classes Namespaces Files Functions Variables Enumerations Enumerator Properties Pages
Drawing.cs
Go to the documentation of this file.
1 #region License
2 /*This file is part of Satsuma Graph Library
3 Copyright © 2013 Balázs Szalkai
4 
5 This software is provided 'as-is', without any express or implied
6 warranty. In no event will the authors be held liable for any damages
7 arising from the use of this software.
8 
9 Permission is granted to anyone to use this software for any purpose,
10 including commercial applications, and to alter it and redistribute it
11 freely, subject to the following restrictions:
12 
13  1. The origin of this software must not be misrepresented; you must not
14  claim that you wrote the original software. If you use this software
15  in a product, an acknowledgment in the product documentation would be
16  appreciated but is not required.
17 
18  2. Altered source versions must be plainly marked as such, and must not be
19  misrepresented as being the original software.
20 
21  3. This notice may not be removed or altered from any source
22  distribution.*/
23 #endregion
24 
25 using System;
26 using System.Collections.Generic;
27 using System.Linq;
28 using System.Xml.Linq;
29 using System.Drawing;
30 using System.Drawing.Drawing2D;
31 using System.Drawing.Imaging;
32 
33 namespace Satsuma.Drawing
34 {
36  public interface INodeShape
37  {
39  PointF Size { get; }
42  void Draw(Graphics graphics, Pen pen, Brush brush);
45  PointF GetBoundary(double angle);
46  }
47 
49  public enum NodeShapeKind
50  {
52  }
53 
55  public sealed class NodeShape : INodeShape
56  {
58  public NodeShapeKind Kind { get; private set; }
60  public PointF Size { get; private set; }
61 
62  private readonly RectangleF rect;
63  private readonly PointF[] points;
64 
65  public NodeShape(NodeShapeKind kind, PointF size)
66  {
67  Kind = kind;
68  Size = size;
69 
70  rect = new RectangleF(-size.X * 0.5f, -size.Y * 0.5f, size.X, size.Y);
71  switch (Kind)
72  {
73  case NodeShapeKind.Diamond: points = new PointF[] { P(rect, 0, 0.5f), P(rect, 0.5f, 1),
74  P(rect, 1, 0.5f), P(rect, 0.5f, 0) }; break;
75  case NodeShapeKind.Rectangle: points = new PointF[] { rect.Location, new PointF(rect.Left, rect.Bottom),
76  new PointF(rect.Right, rect.Bottom), new PointF(rect.Right, rect.Top) }; break;
77  case NodeShapeKind.Triangle: points = new PointF[] { P(rect, 0.5f, 0), P(rect, 0, 1),
78  P(rect, 1, 1) }; break;
79  case NodeShapeKind.UpsideDownTriangle: points = new PointF[] { P(rect, 0.5f, 1), P(rect, 0, 0),
80  P(rect, 1, 0) }; break;
81  }
82  }
83 
84  private static PointF P(RectangleF rect, float x, float y)
85  {
86  return new PointF(rect.Left + rect.Width * x, rect.Top + rect.Height * y);
87  }
88 
89  public void Draw(Graphics graphics, Pen pen, Brush brush)
90  {
91  switch (Kind)
92  {
93  case NodeShapeKind.Ellipse:
94  graphics.FillEllipse(brush, rect);
95  graphics.DrawEllipse(pen, rect);
96  break;
97 
98  default:
99  graphics.FillPolygon(brush, points);
100  graphics.DrawPolygon(pen, points);
101  break;
102  }
103  }
104 
105  public PointF GetBoundary(double angle)
106  {
107  double cos = Math.Cos(angle), sin = Math.Sin(angle);
108  switch (Kind)
109  {
110  case NodeShapeKind.Ellipse:
111  return new PointF((float)(Size.X * 0.5f * cos), (float)(Size.Y * 0.5f * sin));
112 
113  default:
114  // we have a polygon, try to intersect all sides with the ray
115  for (int i = 0; i < points.Length; i++)
116  {
117  int i2 = (i + 1) % points.Length;
118  float t = (float)((points[i].Y * cos - points[i].X * sin) /
119  ((points[i2].X - points[i].X) * sin - (points[i2].Y - points[i].Y) * cos));
120  if (t >= 0 && t <= 1)
121  {
122  var result = new PointF(points[i].X + t * (points[i2].X - points[i].X),
123  points[i].Y + t * (points[i2].Y - points[i].Y));
124  if (result.X * cos + result.Y * sin > 0) return result;
125  }
126  }
127  return new PointF(0, 0); // should not happen
128  }
129  }
130  }
131 
133  public sealed class NodeStyle
134  {
137  public Pen Pen { get; set; }
140  public Brush Brush { get; set; }
143  public INodeShape Shape { get; set; }
146  public Font TextFont { get; set; }
149  public Brush TextBrush { get; set; }
150 
152  public static readonly INodeShape DefaultShape = new NodeShape(NodeShapeKind.Ellipse, new PointF(10, 10));
153 
154  public NodeStyle()
155  {
156  Pen = Pens.Black;
157  Brush = Brushes.White;
158  Shape = DefaultShape;
159  TextFont = SystemFonts.DefaultFont;
160  TextBrush = Brushes.Black;
161  }
162 
163  internal void DrawNode(Graphics graphics, float x, float y, string text)
164  {
165  var state = graphics.Save();
166  graphics.TranslateTransform(x, y);
167  Shape.Draw(graphics, Pen, Brush);
168  if (text != "")
169  graphics.DrawString(text, TextFont, TextBrush, 0, 0,
170  new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center });
171  graphics.Restore(state);
172  }
173  }
174 
195  public sealed class GraphDrawer
196  {
198  public IGraph Graph { get; set; }
200  public Func<Node, PointF> NodePosition { get; set; }
203  public Func<Node, string> NodeCaption { get; set; }
209  public Func<Node, NodeStyle> NodeStyle { get; set; }
212  public Func<Arc, Pen> ArcPen { get; set; }
216  public Pen DirectedPen { get; set; }
220  public Pen UndirectedPen { get; set; }
221 
222  public GraphDrawer()
223  {
224  NodeCaption = (node => "");
225  var defaultNodeStyle = new NodeStyle();
226  NodeStyle = (node => defaultNodeStyle);
227  ArcPen = (arc => Graph.IsEdge(arc) ? UndirectedPen : DirectedPen);
228  DirectedPen = new Pen(Color.Black) { CustomEndCap = new AdjustableArrowCap(3, 5) };
229  UndirectedPen = Pens.Black;
230  }
231 
236  public void Draw(Graphics graphics, Matrix matrix = null)
237  {
238  // draw arcs
239  PointF[] arcPos = new PointF[2];
240  PointF[] boundary = new PointF[2];
241  foreach (var arc in Graph.Arcs())
242  {
243  Node u = Graph.U(arc);
244  arcPos[0] = NodePosition(u);
245  Node v = Graph.V(arc);
246  arcPos[1] = NodePosition(v);
247  if (matrix != null) matrix.TransformPoints(arcPos);
248 
249  // an arc should run between shape boundaries
250  double angle = Math.Atan2(arcPos[1].Y - arcPos[0].Y, arcPos[1].X - arcPos[0].X);
251  boundary[0] = NodeStyle(u).Shape.GetBoundary(angle);
252  boundary[1] = NodeStyle(v).Shape.GetBoundary(angle + Math.PI);
253 
254  graphics.DrawLine(ArcPen(arc), arcPos[0].X + boundary[0].X, arcPos[0].Y + boundary[0].Y,
255  arcPos[1].X + boundary[1].X, arcPos[1].Y + boundary[1].Y);
256  }
257 
258  // draw nodes
259  PointF[] nodePos = new PointF[1];
260  foreach (var node in Graph.Nodes())
261  {
262  nodePos[0] = NodePosition(node);
263  if (matrix != null) matrix.TransformPoints(nodePos);
264  NodeStyle(node).DrawNode(graphics, nodePos[0].X, nodePos[0].Y, NodeCaption(node));
265  }
266  }
267 
270  public void Draw(Graphics graphics, RectangleF box)
271  {
272  if (!Graph.Nodes().Any()) return;
273 
274  float maxShapeWidth = 0, maxShapeHeight = 0;
275  float xmin = float.PositiveInfinity, ymin = float.PositiveInfinity;
276  float xmax = float.NegativeInfinity, ymax = float.NegativeInfinity;
277  foreach (var node in Graph.Nodes())
278  {
279  PointF size = NodeStyle(node).Shape.Size;
280  maxShapeWidth = Math.Max(maxShapeWidth, size.X);
281  maxShapeHeight = Math.Max(maxShapeHeight, size.Y);
282 
283  PointF pos = NodePosition(node);
284  xmin = Math.Min(xmin, pos.X);
285  xmax = Math.Max(xmax, pos.X);
286  ymin = Math.Min(ymin, pos.Y);
287  ymax = Math.Max(ymax, pos.Y);
288  }
289 
290  float xspan = xmax - xmin;
291  if (xspan == 0) xspan = 1;
292  float yspan = ymax - ymin;
293  if (yspan == 0) yspan = 1;
294 
295  Matrix matrix = new Matrix();
296  matrix.Translate(maxShapeWidth*0.6f, maxShapeHeight*0.6f);
297  matrix.Scale((box.Width - maxShapeWidth * 1.2f) / xspan, (box.Height - maxShapeHeight * 1.2f) / yspan);
298  matrix.Translate(-xmin, -ymin);
299  Draw(graphics, matrix);
300  }
301 
308  public Bitmap Draw(int width, int height, Color backColor,
309  bool antialias = true, PixelFormat pixelFormat = PixelFormat.Format32bppArgb)
310  {
311  Bitmap bm = new Bitmap(width, height, pixelFormat);
312  using (var g = Graphics.FromImage(bm))
313  {
314  g.SmoothingMode = antialias ? SmoothingMode.AntiAlias : SmoothingMode.None;
315  g.Clear(backColor);
316  Draw(g, new RectangleF(0, 0, width, height));
317  }
318  return bm;
319  }
320  }
321 }