UNPKG

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