/* ......... 2015 Ivan Mahonin This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ using System; using System.Collections.Generic; using System.Data.Linq; using System.Drawing; using System.IO; using System.Globalization; namespace Diagram { public class ActiveBlock { public Block block; public readonly List links = new List(); public PointF position; public Font captionFont; public Font textFont; public float margin; public float padding; public Pen pen; public Brush brush; public string caption; public string text; public SizeF captionSize; public SizeF textSize; public float distance; public SizeF size; public SizeF clientSize; public PointF getCenter() { return new PointF( position.X + 0.5f*size.Width, position.Y + 0.5f*size.Height ); } public void measure() { float width = captionFont.GetHeight() * 15f; caption = TextUtils.wrap(block.caption, width, captionFont); captionSize = TextUtils.measure(caption, captionFont); text = TextUtils.wrap(block.text, width, textFont); textSize = TextUtils.measure(text, textFont); if (caption != "" && text != "") distance = 0.3f * Math.Max(captionFont.GetHeight(), textFont.GetHeight()); clientSize = new SizeF( Math.Max(captionSize.Width, textSize.Width), captionSize.Height + distance + textSize.Height ); size = new SizeF(clientSize.Width + 2f*padding, clientSize.Height + 2f*padding); } public void draw(Graphics g) { Shapes.drawRoundRect(g, new RectangleF(position, size), 2f*padding, pen, brush); g.DrawString( caption, captionFont, Brushes.Black, new RectangleF( position.X + 0.5f*(size.Width - captionSize.Width), position.Y + padding, captionSize.Width, captionSize.Height )); g.DrawString( text, textFont, Brushes.Black, new RectangleF( position.X + padding, position.Y + padding + captionSize.Height + distance, clientSize.Width, clientSize.Height - captionSize.Height - distance )); } class LinkDesc { public ActiveLink link; public PointF src; public PointF dst; } class SideDesc { public PointF a; public PointF b; public PointF normal; public readonly List links = new List(); public SideDesc(PointF a, PointF b) { this.a = a; this.b = b; float l = Geometry.lineLength(a, b); normal = new PointF((b.Y - a.Y)/l, (a.X - b.X)/l); } } public void placeLinks() { PointF lt = position; PointF rb = new PointF(lt.X + size.Width, lt.Y + size.Height); PointF lb = new PointF(lt.X, rb.Y); PointF rt = new PointF(rb.X, lt.Y); PointF center = getCenter(); List sides = new List() { new SideDesc(lt, rt), new SideDesc(rt, rb), new SideDesc(rb, lb), new SideDesc(lb, lt) }; foreach(ActiveLink link in links) { link.visible = false; LinkDesc linkDesc = new LinkDesc(); linkDesc.link = link; if (link.src == this) { linkDesc.src = center; linkDesc.dst = link.dst.getCenter(); } else { linkDesc.src = link.src.getCenter(); linkDesc.dst = center; } foreach(SideDesc side in sides) { PointF p; if (Geometry.findIntersection(side.a, side.b, linkDesc.src, linkDesc.dst, out p)) { link.visible = true; linkDesc.src = p; side.links.Add(linkDesc); break; } } } foreach(SideDesc side in sides) { side.links.Sort(delegate(LinkDesc a, LinkDesc b) { return Geometry.comparePointsAtLine(a.src, b.src, side.a, side.b); }); for(int i = 0; i < side.links.Count; ++i) { ActiveLink link = side.links[i].link; PointF p = Geometry.pointAtLine(side.a, side.b, i, side.links.Count, padding); if (link.src == this) { link.srcBase = p; link.srcTangent = side.normal; link.pen = pen; } else { link.dstBase = p; link.dstTangent = new PointF(-side.normal.X, -side.normal.Y); } } } } } public class ActiveLink { public Link link; public ActiveBlock src; public ActiveBlock dst; public SizeF arrowSize; public Pen pen; public bool visible; public PointF srcBase; public PointF srcTangent; public PointF dstBase; public PointF dstTangent; public PointF[] bezier; public PointF[] arrow; public static readonly PointF[] arrowTemplate = new PointF[] { new PointF(0f, 0f), new PointF(1f, 1f), new PointF(0f, 0.75f) }; public static readonly float arrowOffset = 0.75f; public static PointF[] makeArrow(PointF point, PointF tangent, SizeF size) { PointF[] arrow = new PointF[2*arrowTemplate.Length]; PointF tx = new PointF( -0.5f*tangent.Y*size.Width, 0.5f*tangent.X*size.Width ); PointF ty = new PointF( -tangent.X*size.Height, -tangent.Y*size.Height ); for(int i = 0; i < arrowTemplate.Length; ++i) { arrow[i] = new PointF( point.X + tx.X*arrowTemplate[i].X + ty.X*arrowTemplate[i].Y, point.Y + tx.Y*arrowTemplate[i].X + ty.Y*arrowTemplate[i].Y ); arrow[2*arrowTemplate.Length - i - 1] = new PointF( point.X - tx.X*arrowTemplate[i].X + ty.X*arrowTemplate[i].Y, point.Y - tx.Y*arrowTemplate[i].X + ty.Y*arrowTemplate[i].Y ); } return arrow; } public void place() { if (!visible) return; float dx = 0.5f*Math.Abs(dstBase.X - srcBase.X); float dy = 0.5f*Math.Abs(dstBase.Y - srcBase.Y); float l = (float)Math.Sqrt(dx*dx + dy*dy); if (l < Geometry.precision) { visible = false; return; } dx = Math.Max(dx, 0.25f*l); dy = Math.Max(dy, 0.25f*l); float sl = l; if (Math.Abs(srcTangent.X) > Geometry.precision) sl = Math.Min(sl, dx/Math.Abs(srcTangent.X)); if (Math.Abs(srcTangent.Y) > Geometry.precision) sl = Math.Min(sl, dy/Math.Abs(srcTangent.Y)); PointF st = new PointF(srcTangent.X*sl, srcTangent.Y*sl); float dl = l; if (Math.Abs(dstTangent.X) > Geometry.precision) dl = Math.Min(dl, dx/Math.Abs(dstTangent.X)); if (Math.Abs(dstTangent.Y) > Geometry.precision) dl = Math.Min(dl, dy/Math.Abs(dstTangent.Y)); PointF dt = new PointF(dstTangent.X*sl, dstTangent.Y*sl); bezier = new PointF[] { srcBase, new PointF(srcBase.X + st.X, srcBase.Y + st.Y), new PointF(dstBase.X - dt.X, dstBase.Y - dt.Y), new PointF(dstBase.X - dstTangent.X*(arrowOffset*arrowSize.Height + 0.5f*pen.Width), dstBase.Y - dstTangent.Y*(arrowOffset*arrowSize.Height + 0.5f*pen.Width)) }; arrow = makeArrow( new PointF(dstBase.X - 0.5f*dstTangent.X*pen.Width, dstBase.Y - 0.5f*dstTangent.Y*pen.Width), dstTangent, arrowSize ); } public void draw(Graphics g) { if (!visible) return; g.DrawBeziers(pen, bezier); g.FillPolygon(new SolidBrush(pen.Color), arrow); //g.DrawPolygon(pen, arrow); } } public class ActiveDiagram { public Diagram diagram; public readonly Dictionary positions = new Dictionary(); public Font captionFont; public Font textFont; public float margin; public float padding; public SizeF arrowSize; public Pen pen; public Brush brush; public readonly Dictionary blocks = new Dictionary(); public readonly Dictionary links = new Dictionary(); public RectangleF bounds; public void savePositions(string filename) { remeberPositions(); CultureInfo ci = new CultureInfo("en-US"); List lines = new List(); foreach(KeyValuePair pair in positions) { lines.Add(pair.Key); lines.Add(pair.Value.X.ToString(ci)); lines.Add(pair.Value.Y.ToString(ci)); } File.Create(filename).Close(); File.WriteAllLines(filename, lines); } public void loadPositions(string filename) { CultureInfo ci = new CultureInfo("en-US"); string[] lines = File.ReadAllLines(filename); for(int i = 0; i < lines.Length - 2; i += 3) { string k = lines[i]; PointF p = new PointF( float.Parse(lines[i+1], ci), float.Parse(lines[i+2], ci) ); if (positions.ContainsKey(k)) positions[k] = p; else positions.Add(k, p); } restorePositions(); } public void remeberPositions() { foreach(KeyValuePair pair in blocks) if (positions.ContainsKey(pair.Key)) positions[pair.Key] = pair.Value.position; else positions.Add(pair.Key, pair.Value.position); } public void restorePositions() { foreach(KeyValuePair pair in blocks) if (positions.ContainsKey(pair.Key)) pair.Value.position = positions[pair.Key]; placeLinks(); } // insert/remove blocks and links calculate sizes public void reloadDiagram() { remeberPositions(); bool retry; // blocks foreach(KeyValuePair pair in blocks) pair.Value.block = null; foreach(KeyValuePair pair in diagram.blocks) { if (!blocks.ContainsKey(pair.Key)) blocks.Add(pair.Key, new ActiveBlock()); ActiveBlock b = blocks[pair.Key]; b.block = pair.Value; b.links.Clear(); b.captionFont = captionFont; b.textFont = textFont; b.margin = margin; b.padding = padding; b.pen = new Pen(new SolidBrush(b.block.color), pen.Width); b.brush = brush; } retry = true; while(retry) { retry = false; foreach(KeyValuePair pair in blocks) if (pair.Value.block == null) { blocks.Remove(pair.Key); retry = true; break; } } // links foreach(KeyValuePair pair in links) pair.Value.link = null; foreach(KeyValuePair pair in diagram.links) { if ( blocks.ContainsKey(pair.Value.srcId) && blocks.ContainsKey(pair.Value.dstId) ) { if (!links.ContainsKey(pair.Key)) links.Add(pair.Key, new ActiveLink()); ActiveLink l = links[pair.Key]; l.link = pair.Value; l.src = blocks[l.link.srcId]; l.src.links.Add(l); l.dst = blocks[l.link.dstId]; l.dst.links.Add(l); l.arrowSize = arrowSize; l.pen = pen; } } retry = true; while(retry) { retry = false; foreach(KeyValuePair pair in links) if (pair.Value.link == null) { links.Remove(pair.Key); retry = true; break; } } measureBlocks(); } public void measureBlocks() { foreach(KeyValuePair pair in blocks) pair.Value.measure(); placeLinks(); } public void placeLinks() { foreach(KeyValuePair pair in blocks) pair.Value.placeLinks(); foreach(KeyValuePair pair in links) pair.Value.place(); recalcBounds(); } public void recalcBounds() { if (blocks.Count == 0) { bounds = new RectangleF(); return; } float minx = float.PositiveInfinity; float miny = float.PositiveInfinity; float maxx = float.NegativeInfinity; float maxy = float.NegativeInfinity; foreach(KeyValuePair pair in blocks) { minx = Math.Min(minx, pair.Value.position.X - pair.Value.margin); miny = Math.Min(miny, pair.Value.position.Y - pair.Value.margin); maxx = Math.Max(maxx, pair.Value.position.X + pair.Value.size.Width + pair.Value.margin); maxy = Math.Max(maxy, pair.Value.position.Y + pair.Value.size.Height + pair.Value.margin); } bounds = new RectangleF(minx, miny, maxx - minx, maxy - miny); } public void draw(Graphics g) { foreach(KeyValuePair pair in links) pair.Value.draw(g); foreach(KeyValuePair pair in blocks) pair.Value.draw(g); } } }