UNPKG

14 kBJavaScriptView Raw
1/*
2* Copyright (C) 1998-2021 by Northwoods Software Corporation. All Rights Reserved.
3*/
4/*
5* This is an extension and not part of the main GoJS library.
6* Note that the API for this class may change with any version, even point releases.
7* If you intend to use an extension in production, you should copy the code to your own source directory.
8* Extensions can be found in the GoJS kit under the extensions or extensionsTS folders.
9* See the Extensions intro page (https://gojs.net/latest/intro/extensions.html) for more information.
10*/
11import * as go from '../release/go-module.js';
12/**
13 * A custom {@link TreeLayout} that can be used for laying out stylized flowcharts.
14 * Each layout requires a single "Split" node and a single "Merge" node.
15 * The "Split" node should be the root of a tree-like structure if one excludes links to the "Merge" node.
16 * This will position the "Merge" node to line up with the "Split" node.
17 *
18 * You can set all of the TreeLayout properties that you like,
19 * except that for simplicity this code just works for angle === 0 or angle === 90.
20 *
21 * If you want to experiment with this extension, try the <a href="../../extensionsJSM/Parallel.html">Parallel Layout</a> sample.
22 * @category Layout Extension
23 */
24export class ParallelLayout extends go.TreeLayout {
25 /**
26 * Constructs a ParallelLayout and sets the following properties:
27 * - {@link #isRealtime} = false
28 * - {@link #alignment} = {@link TreeLayout.AlignmentCenterChildren}
29 * - {@link #compaction} = {@link TreeLayout.CompactionNone}
30 * - {@link #alternateAlignment} = {@link TreeLayout.AlignmentCenterChildren}
31 * - {@link #alternateCompaction} = {@link TreeLayout.CompactionNone}
32 */
33 constructor() {
34 super();
35 this._splitNode = null;
36 this._mergeNode = null;
37 this.isRealtime = false;
38 this.alignment = go.TreeLayout.AlignmentCenterChildren;
39 this.compaction = go.TreeLayout.CompactionNone;
40 this.alternateAlignment = go.TreeLayout.AlignmentCenterChildren;
41 this.alternateCompaction = go.TreeLayout.CompactionNone;
42 }
43 /**
44 * This read-only property returns the node that the tree will extend from.
45 */
46 get splitNode() { return this._splitNode; }
47 set splitNode(val) { this._splitNode = val; }
48 /**
49 * This read-only property returns the node that the tree will converge at.
50 */
51 get mergeNode() { return this._mergeNode; }
52 set mergeNode(val) { this._mergeNode = val; }
53 /**
54 * Overridable predicate for deciding if a Node is a Split node.
55 * By default this checks the node's {@link Part#category} to see if it is
56 * "Split", "Start", "For", "While", "If", or "Switch".
57 * @param {Node} node
58 * @return {boolean}
59 */
60 isSplit(node) {
61 if (!(node instanceof go.Node))
62 return false;
63 var cat = node.category;
64 return (cat === "Split" || cat === "Start" || cat === "For" || cat === "While" || cat === "If" || cat === "Switch");
65 }
66 /**
67 * Overridable predicate for deciding if a Node is a Merge node.
68 * By default this checks the node's {@link Part#category} to see if it is
69 * "Merge", "End", "EndFor", "EndWhile", "EndIf", or "EndSwitch".
70 * @param {Node} node
71 * @return {boolean}
72 */
73 isMerge(node) {
74 if (!(node instanceof go.Node))
75 return false;
76 var cat = node.category;
77 return (cat === "Merge" || cat === "End" || cat === "EndFor" || cat === "EndWhile" || cat === "EndIf" || cat === "EndSwitch");
78 }
79 /**
80 * Overridable predicate for deciding if a Node is a conditional or "If" type of Split Node
81 * expecting to have two links coming out of the sides.
82 * @param {Node} node
83 * @return {boolean}
84 */
85 isConditional(node) {
86 if (!(node instanceof go.Node))
87 return false;
88 return node.category === "If";
89 }
90 /**
91 * Overridable predicate for deciding if a Node is a "Switch" type of Split Node
92 * expecting to have three links coming out of the bottom/right side.
93 * @param {Node} node
94 * @return {boolean}
95 */
96 isSwitch(node) {
97 if (!(node instanceof go.Node))
98 return false;
99 return node.category === "Switch";
100 }
101 /**
102 * Return an Array holding the Split node and the Merge node for this layout.
103 * This signals an error if there is not exactly one Node that {@link #isSplit}
104 * and exactly one Node that {@link #isMerge}.
105 * This can be overridden; any override must set {@link #splitNode} and {@link #mergeNode}.
106 * @param {Iterable<TreeVertex>} vertexes
107 */
108 findSplitMerge(vertexes) {
109 var split = null;
110 var merge = null;
111 var it = vertexes.iterator;
112 while (it.next()) {
113 var v = it.value;
114 if (!v.node)
115 continue;
116 if (this.isSplit(v.node)) {
117 if (split)
118 throw new Error("Split node already exists in " + this + " -- existing: " + split + " new: " + v.node);
119 split = v.node;
120 }
121 else if (this.isMerge(v.node)) {
122 if (merge)
123 throw new Error("Merge node already exists in " + this + " -- existing: " + merge + " new: " + v.node);
124 merge = v.node;
125 }
126 }
127 if (!split)
128 throw new Error("Missing Split node in " + this);
129 if (!merge)
130 throw new Error("Missing Merge node in " + this);
131 this._splitNode = split;
132 this._mergeNode = merge;
133 }
134 /**
135 * @hidden @internal
136 */
137 makeNetwork(coll) {
138 const net = super.makeNetwork(coll);
139 // Groups might be unbalanced -- position them so that the Split node is centered under the parent node.
140 var it = net.vertexes.iterator;
141 while (it.next()) {
142 var v = it.value;
143 var g = v.node;
144 if (g instanceof go.Group && g.isSubGraphExpanded && g.placeholder !== null && g.layout instanceof ParallelLayout) {
145 var split = g.layout.splitNode;
146 if (split) {
147 if (this.angle === 0) {
148 v.focusY = split.location.y - g.position.y;
149 }
150 else if (this.angle === 90) {
151 v.focusX = split.location.x - g.position.x;
152 }
153 }
154 }
155 }
156 if (this.group && !this.group.isSubGraphExpanded)
157 return net;
158 // look for and remember the one Split node and the one Merge node
159 this.findSplitMerge(net.vertexes.iterator);
160 // don't have TreeLayout lay out the Merge node; commitNodes will do it
161 if (this.mergeNode)
162 net.deleteNode(this.mergeNode);
163 // for each vertex that does not have an incoming edge,
164 // connect to it from the splitNode vertex with a dummy edge
165 if (this.splitNode) {
166 var splitv = net.findVertex(this.splitNode);
167 net.vertexes.each(function (v) {
168 if (splitv === null || v === splitv)
169 return;
170 if (v.sourceEdges.count === 0) {
171 net.linkVertexes(splitv, v, null);
172 }
173 });
174 }
175 return net;
176 }
177 /**
178 * @hidden @internal
179 */
180 commitNodes() {
181 super.commitNodes();
182 // Line up the Merge node to the center of the Split node
183 var mergeNode = this.mergeNode;
184 var splitNode = this.splitNode;
185 if (mergeNode === null || splitNode === null || this.network === null)
186 return;
187 var splitVertex = this.network.findVertex(splitNode);
188 if (splitVertex === null)
189 return;
190 if (this.angle === 0) {
191 mergeNode.location = new go.Point(splitVertex.x + splitVertex.subtreeSize.width + this.layerSpacing + mergeNode.actualBounds.width / 2, splitVertex.centerY);
192 }
193 else if (this.angle === 90) {
194 mergeNode.location = new go.Point(splitVertex.centerX, splitVertex.y + splitVertex.subtreeSize.height + this.layerSpacing + mergeNode.actualBounds.height / 2);
195 }
196 mergeNode.ensureBounds();
197 }
198 /**
199 * @hidden @internal
200 */
201 commitLinks() {
202 const splitNode = this.splitNode;
203 const mergeNode = this.mergeNode;
204 if (splitNode === null || mergeNode === null || this.network === null)
205 return;
206 // set default link spots based on this.angle
207 const it = this.network.edges.iterator;
208 while (it.next()) {
209 const e = it.value;
210 const link = e.link;
211 if (!link)
212 continue;
213 if (this.angle === 0) {
214 if (this.setsPortSpot)
215 link.fromSpot = go.Spot.Right;
216 if (this.setsChildPortSpot)
217 link.toSpot = go.Spot.Left;
218 }
219 else if (this.angle === 90) {
220 if (this.setsPortSpot)
221 link.fromSpot = go.Spot.Bottom;
222 if (this.setsChildPortSpot)
223 link.toSpot = go.Spot.Top;
224 }
225 }
226 // Make sure links coming into and going out of a Split node come in the correct way
227 if (splitNode) {
228 // Handle links coming into the Split node
229 const cond = this.isConditional(splitNode);
230 const swtch = this.isSwitch(splitNode);
231 // Handle links going out of the Split node
232 let first = true; // handle "If" nodes specially
233 const lit = splitNode.findLinksOutOf();
234 while (lit.next()) {
235 const link = lit.value;
236 if (this.angle === 0) {
237 if (this.setsPortSpot)
238 link.fromSpot = cond ? (first ? go.Spot.Top : go.Spot.Bottom) : (swtch ? go.Spot.RightSide : go.Spot.Right);
239 if (this.setsChildPortSpot)
240 link.toSpot = go.Spot.Left;
241 }
242 else if (this.angle === 90) {
243 if (this.setsPortSpot)
244 link.fromSpot = cond ? (first ? go.Spot.Left : go.Spot.Right) : (swtch ? go.Spot.BottomSide : go.Spot.Bottom);
245 if (this.setsChildPortSpot)
246 link.toSpot = go.Spot.Top;
247 }
248 first = false;
249 }
250 }
251 if (mergeNode) {
252 // Handle links going into the Merge node
253 const iit = mergeNode.findLinksInto();
254 while (iit.next()) {
255 const link = iit.value;
256 if (!this.isSplit(link.fromNode)) { // if link connects Split with Merge directly, only set fromSpot once
257 if (this.angle === 0) {
258 if (this.setsPortSpot)
259 link.fromSpot = go.Spot.Right;
260 if (this.setsChildPortSpot)
261 link.toSpot = go.Spot.Left;
262 }
263 else if (this.angle === 90) {
264 if (this.setsPortSpot)
265 link.fromSpot = go.Spot.Bottom;
266 if (this.setsChildPortSpot)
267 link.toSpot = go.Spot.Top;
268 }
269 }
270 if (!link.isOrthogonal)
271 continue;
272 // have all of the links coming into the Merge node have segments
273 // that share a common X (or if angle==90, Y) coordinate
274 link.updateRoute();
275 if (link.pointsCount >= 6) {
276 const pts = link.points.copy();
277 const p2 = pts.elt(pts.length - 4);
278 const p3 = pts.elt(pts.length - 3);
279 if (this.angle === 0 && p2.x === p3.x) {
280 const x = mergeNode.position.x - this.layerSpacing / 2;
281 pts.setElt(pts.length - 4, new go.Point(x, p2.y));
282 pts.setElt(pts.length - 3, new go.Point(x, p3.y));
283 }
284 else if (this.angle === 90 && p2.y === p3.y) {
285 const y = mergeNode.position.y - this.layerSpacing / 2;
286 pts.setElt(pts.length - 4, new go.Point(p2.x, y));
287 pts.setElt(pts.length - 3, new go.Point(p3.x, y));
288 }
289 link.points = pts;
290 }
291 }
292 // handle links coming out of the Merge node, looping back left/up
293 const oit = mergeNode.findLinksOutOf();
294 while (oit.next()) {
295 const link = oit.value;
296 // if connects internal with external node, it isn't a loop-back link
297 if (link.toNode && link.toNode.containingGroup !== mergeNode.containingGroup)
298 continue;
299 if (this.angle === 0) {
300 if (this.setsPortSpot)
301 link.fromSpot = go.Spot.TopBottomSides;
302 if (this.setsChildPortSpot)
303 link.toSpot = go.Spot.TopBottomSides;
304 }
305 else if (this.angle === 90) {
306 if (this.setsPortSpot)
307 link.fromSpot = go.Spot.LeftRightSides;
308 if (this.setsChildPortSpot)
309 link.toSpot = go.Spot.LeftRightSides;
310 }
311 link.routing = go.Link.AvoidsNodes;
312 }
313 }
314 }
315}