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 | */
|
11 | import * 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 | */
|
24 | export 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 | }
|