1 | using System;
|
---|
2 | using System.Collections.Generic;
|
---|
3 | using System.ComponentModel;
|
---|
4 | using System.Drawing;
|
---|
5 | using Netron.Diagramming.Core.Analysis;
|
---|
6 | namespace Netron.Diagramming.Core {
|
---|
7 | /// <summary>
|
---|
8 | /// <para>Layout instance implementing the Fruchterman-Reingold algorithm for
|
---|
9 | /// force-directed placement of graph nodes. The computational complexity of
|
---|
10 | /// this algorithm is quadratic [O(n^2)] in the number of nodes, so should
|
---|
11 | /// only be applied for relatively small graphs, particularly in interactive
|
---|
12 | /// situations.</para>
|
---|
13 | ///
|
---|
14 | /// <para>This implementation was ported from the implementation in the
|
---|
15 | /// <a href="http://jung.sourceforge.net/">JUNG</a> framework.</para>
|
---|
16 | /// </summary>
|
---|
17 | class FruchtermanReingoldLayout : LayoutBase {
|
---|
18 | #region Fields
|
---|
19 | private double forceConstant;
|
---|
20 | private double temp;
|
---|
21 | private int maxIter = 300;
|
---|
22 |
|
---|
23 | protected int m_fidx;
|
---|
24 | private Random rnd;
|
---|
25 | private static double EPSILON = 0.000001D;
|
---|
26 | private static double ALPHA = 0.1D;
|
---|
27 |
|
---|
28 | Dictionary<string, Params> Pars;
|
---|
29 | double width;
|
---|
30 | double height;
|
---|
31 | BackgroundWorker worker;
|
---|
32 | #endregion
|
---|
33 |
|
---|
34 | #region Constructor
|
---|
35 | ///<summary>
|
---|
36 | ///Default constructor
|
---|
37 | ///</summary>
|
---|
38 | public FruchtermanReingoldLayout(IController controller)
|
---|
39 | : base("FruchtermanReingold Layout", controller) {
|
---|
40 | }
|
---|
41 | #endregion
|
---|
42 |
|
---|
43 | #region Methods
|
---|
44 |
|
---|
45 | /// <summary>
|
---|
46 | /// Inits the calculational state
|
---|
47 | /// </summary>
|
---|
48 | private bool Init() {
|
---|
49 | this.Graph = this.Model as IGraph;
|
---|
50 |
|
---|
51 | if (Graph == null)
|
---|
52 | throw new InconsistencyException("The model has not been set and the Graph property is hence 'null'");
|
---|
53 |
|
---|
54 | //this.LayoutRoot = this.Controller.Model.LayoutRoot;//could be null if not set in the GUI
|
---|
55 | //Graph.ClearSpanningTree();
|
---|
56 | //Graph.MakeSpanningTree(LayoutRoot as INode);
|
---|
57 |
|
---|
58 | Pars = new Dictionary<string, Params>();
|
---|
59 | if (Nodes.Count == 0)
|
---|
60 | return false;
|
---|
61 | if (Edges.Count == 0) //this layout is base on embedded springs in the connections
|
---|
62 | return false;
|
---|
63 |
|
---|
64 | width = (double)Bounds.Width;
|
---|
65 | height = (double)Bounds.Height;
|
---|
66 | rnd = new Random();
|
---|
67 |
|
---|
68 |
|
---|
69 | temp = width / 10;
|
---|
70 | forceConstant = 0.75 * Math.Sqrt(height * width / Nodes.Count);
|
---|
71 |
|
---|
72 | // initialize node positions
|
---|
73 |
|
---|
74 |
|
---|
75 | double scaleW = ALPHA * width / 2;
|
---|
76 | double scaleH = ALPHA * height / 2;
|
---|
77 | Params par;
|
---|
78 | double[] loc;
|
---|
79 | foreach (INode node in Nodes) {
|
---|
80 | loc = new double[2];
|
---|
81 | loc[0] = Center.X + rnd.NextDouble() * scaleW;
|
---|
82 | loc[1] = Center.Y + rnd.NextDouble() * scaleH;
|
---|
83 | par = new Params(loc, new double[2]);
|
---|
84 | Pars.Add(node.Uid.ToString(), par);
|
---|
85 | }
|
---|
86 | return true;
|
---|
87 | }
|
---|
88 |
|
---|
89 | /// <summary>
|
---|
90 | /// Runs this instance.
|
---|
91 | /// </summary>
|
---|
92 | public override void Run() {
|
---|
93 | width = this.Bounds.Width;
|
---|
94 | height = this.Bounds.Height;
|
---|
95 | Run(1000);
|
---|
96 |
|
---|
97 |
|
---|
98 | }
|
---|
99 | /// <summary>
|
---|
100 | /// Stops this instance.
|
---|
101 | /// </summary>
|
---|
102 | public override void Stop() {
|
---|
103 | if (worker != null && worker.IsBusy)
|
---|
104 | worker.CancelAsync();
|
---|
105 | }
|
---|
106 | /// <summary>
|
---|
107 | /// Runs the layout for a specified time.
|
---|
108 | /// </summary>
|
---|
109 | /// <param name="time">The time.</param>
|
---|
110 | public override void Run(int time) {
|
---|
111 | if (Init()) {
|
---|
112 |
|
---|
113 | worker = new BackgroundWorker();
|
---|
114 | worker.DoWork += new DoWorkEventHandler(worker_DoWork);
|
---|
115 | worker.RunWorkerAsync(time);
|
---|
116 |
|
---|
117 | }
|
---|
118 |
|
---|
119 | }
|
---|
120 |
|
---|
121 | /// <summary>
|
---|
122 | /// Handles the DoWork event of the worker control.
|
---|
123 | /// </summary>
|
---|
124 | /// <param name="sender">The source of the event.</param>
|
---|
125 | /// <param name="e">The <see cref="T:System.ComponentModel.DoWorkEventArgs"/> instance containing the event data.</param>
|
---|
126 | void worker_DoWork(object sender, DoWorkEventArgs e) {
|
---|
127 | //this.Controller.View.Suspend();
|
---|
128 | DateTime start = DateTime.Now;
|
---|
129 | while (DateTime.Now < start.AddMilliseconds((int)e.Argument)) {
|
---|
130 | RunStep();
|
---|
131 |
|
---|
132 | }
|
---|
133 | //this.Controller.View.Resume();
|
---|
134 | UpdateShapes();
|
---|
135 |
|
---|
136 | RunAnimation();
|
---|
137 | }
|
---|
138 |
|
---|
139 | private void RunAnimation() {
|
---|
140 |
|
---|
141 | }
|
---|
142 |
|
---|
143 | /// <summary>
|
---|
144 | /// Runs a single step.
|
---|
145 | /// </summary>
|
---|
146 | private void RunStep() {
|
---|
147 |
|
---|
148 | for (int curIter = 0; curIter < maxIter; curIter++) {
|
---|
149 |
|
---|
150 | // Calculate repulsion
|
---|
151 | foreach (INode node in Nodes) {
|
---|
152 | if (node.IsFixed) continue;
|
---|
153 | CalculateRepulsion(node);
|
---|
154 | }
|
---|
155 |
|
---|
156 | // Calculate attraction
|
---|
157 | foreach (IEdge edge in Edges) {
|
---|
158 | CalculateAttraction(edge);
|
---|
159 | }
|
---|
160 |
|
---|
161 | foreach (INode node in Nodes) {
|
---|
162 | if (node.IsFixed) continue;
|
---|
163 | CalculatePositions(node);
|
---|
164 | }
|
---|
165 |
|
---|
166 | CoolDown(curIter);
|
---|
167 |
|
---|
168 |
|
---|
169 | }
|
---|
170 |
|
---|
171 |
|
---|
172 | }
|
---|
173 |
|
---|
174 | private void UpdateShapes() {
|
---|
175 | int x, y;
|
---|
176 | lock (Nodes)
|
---|
177 | lock (Pars) {
|
---|
178 | foreach (INode node in Nodes) {
|
---|
179 | x = Convert.ToInt32(Pars[node.Uid.ToString()].loc[0]) - node.Rectangle.X;
|
---|
180 | y = Convert.ToInt32(Pars[node.Uid.ToString()].loc[1]) - node.Rectangle.Y;
|
---|
181 | //if (node.Rectangle.X + x < width - node.Rectangle.Width - 10 && node.Rectangle.X + x > 10 && node.Rectangle.Y + y < height - node.Rectangle.Height - 10 && node.Rectangle.Y + y > 10)
|
---|
182 | node.MoveBy(new Point(x, y));
|
---|
183 | }
|
---|
184 | }
|
---|
185 | }
|
---|
186 |
|
---|
187 | #region Calculations
|
---|
188 | public void CalculatePositions(INode n) {
|
---|
189 | Params np = Pars[n.Uid.ToString()];
|
---|
190 |
|
---|
191 |
|
---|
192 | double deltaLength = Math.Max(EPSILON, Math.Sqrt(np.disp[0] * np.disp[0] + np.disp[1] * np.disp[1]));
|
---|
193 |
|
---|
194 | double xDisp = np.disp[0] / deltaLength * Math.Min(deltaLength, temp);
|
---|
195 |
|
---|
196 | if (Double.IsNaN(xDisp)) {
|
---|
197 | System.Diagnostics.Trace.WriteLine("Oops, the layout resulted in a NaN problem.");
|
---|
198 | return;
|
---|
199 | }
|
---|
200 |
|
---|
201 | double yDisp = np.disp[1] / deltaLength * Math.Min(deltaLength, temp);
|
---|
202 |
|
---|
203 | np.loc[0] += xDisp;
|
---|
204 | np.loc[1] += yDisp;
|
---|
205 |
|
---|
206 |
|
---|
207 | // don't let nodes leave the display
|
---|
208 | double borderWidth = width / 50.0;
|
---|
209 | double x = np.loc[0];
|
---|
210 | if (x < borderWidth) {
|
---|
211 | x = borderWidth + rnd.NextDouble() * borderWidth * 2.0;
|
---|
212 | } else if (x > (width - borderWidth)) {
|
---|
213 | x = width - borderWidth - rnd.NextDouble() * borderWidth * 2.0;
|
---|
214 | }
|
---|
215 |
|
---|
216 | double y = np.loc[1];
|
---|
217 | if (y < borderWidth) {
|
---|
218 | y = borderWidth + rnd.NextDouble() * borderWidth * 2.0;
|
---|
219 | } else if (y > (height - borderWidth)) {
|
---|
220 | y = height - borderWidth - rnd.NextDouble() * borderWidth * 2.0;
|
---|
221 | }
|
---|
222 |
|
---|
223 | np.loc[0] = x;
|
---|
224 | np.loc[1] = y;
|
---|
225 | }
|
---|
226 |
|
---|
227 | /// <summary>
|
---|
228 | /// Calculates the attraction or tension on the given edge.
|
---|
229 | /// </summary>
|
---|
230 | /// <param name="edge">The edge.</param>
|
---|
231 | public void CalculateAttraction(IEdge edge) {
|
---|
232 | INode n1, n2;
|
---|
233 | Params n1p = new Params();
|
---|
234 | Params n2p = new Params();
|
---|
235 | if (edge.SourceNode != null) {
|
---|
236 | n2 = edge.SourceNode;
|
---|
237 | n2p = Pars[n2.Uid.ToString()];
|
---|
238 | };
|
---|
239 | if (edge.TargetNode != null) {
|
---|
240 | n1 = edge.TargetNode;
|
---|
241 | n1p = Pars[n1.Uid.ToString()];
|
---|
242 | };
|
---|
243 |
|
---|
244 |
|
---|
245 |
|
---|
246 | double xDelta = n1p.loc[0] - n2p.loc[0];
|
---|
247 | double yDelta = n1p.loc[1] - n2p.loc[1];
|
---|
248 |
|
---|
249 | double deltaLength = Math.Max(EPSILON, Math.Sqrt(xDelta * xDelta + yDelta * yDelta));
|
---|
250 | double force = (deltaLength * deltaLength) / forceConstant;
|
---|
251 |
|
---|
252 | if (Double.IsNaN(force)) {
|
---|
253 | System.Diagnostics.Trace.WriteLine("Oops, the layout resulted in a NaN problem.");
|
---|
254 | return;
|
---|
255 | }
|
---|
256 |
|
---|
257 | double xDisp = (xDelta / deltaLength) * force;
|
---|
258 | double yDisp = (yDelta / deltaLength) * force;
|
---|
259 |
|
---|
260 | n1p.disp[0] -= xDisp; n1p.disp[1] -= yDisp;
|
---|
261 | n2p.disp[0] += xDisp; n2p.disp[1] += yDisp;
|
---|
262 | }
|
---|
263 |
|
---|
264 | public void CalculateRepulsion(INode node) {
|
---|
265 | Params np = Pars[node.Uid.ToString()];
|
---|
266 | np.disp[0] = 0.0; np.disp[1] = 0.0;
|
---|
267 |
|
---|
268 | foreach (INode n2 in Nodes) {
|
---|
269 |
|
---|
270 | Params n2p = Pars[n2.Uid.ToString()];
|
---|
271 | if (n2.IsFixed) continue;
|
---|
272 | if (node != n2) {
|
---|
273 | double xDelta = np.loc[0] - n2p.loc[0];
|
---|
274 | double yDelta = np.loc[1] - n2p.loc[1];
|
---|
275 |
|
---|
276 | double deltaLength = Math.Max(EPSILON, Math.Sqrt(xDelta * xDelta + yDelta * yDelta));
|
---|
277 |
|
---|
278 | double force = (forceConstant * forceConstant) / deltaLength;
|
---|
279 |
|
---|
280 | if (Double.IsNaN(force)) {
|
---|
281 | System.Diagnostics.Trace.WriteLine("Oops, the layout resulted in a NaN problem.");
|
---|
282 | return;
|
---|
283 | }
|
---|
284 |
|
---|
285 | np.disp[0] += (xDelta / deltaLength) * force;
|
---|
286 | np.disp[1] += (yDelta / deltaLength) * force;
|
---|
287 | }
|
---|
288 | }
|
---|
289 | }
|
---|
290 |
|
---|
291 | private void CoolDown(int curIter) {
|
---|
292 | temp *= (1.0 - curIter / (double)maxIter);
|
---|
293 | }
|
---|
294 | #endregion
|
---|
295 |
|
---|
296 |
|
---|
297 | /// <summary>
|
---|
298 | /// Paramter blob to temporarily keep working data of one node.
|
---|
299 | /// </summary>
|
---|
300 | struct Params {
|
---|
301 | public double[] loc;
|
---|
302 | public double[] disp;
|
---|
303 |
|
---|
304 | public Params(double[] loc, double[] disp) {
|
---|
305 | this.loc = loc;
|
---|
306 | this.disp = disp;
|
---|
307 | }
|
---|
308 |
|
---|
309 |
|
---|
310 | }
|
---|
311 |
|
---|
312 |
|
---|
313 | #endregion
|
---|
314 |
|
---|
315 | }
|
---|
316 | }
|
---|