/*
......... 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);
}
}
}