UNPKG

12.1 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 * FishboneLayout is a custom {@link Layout} derived from {@link TreeLayout} for creating "fishbone" diagrams.
17 * A fishbone diagram also requires a {@link Link} class that implements custom routing, {@link FishboneLink}.
18 *
19 * This only works for angle === 0 or angle === 180.
20 *
21 * This layout assumes Links are automatically routed in the way needed by fishbone diagrams,
22 * by using the FishboneLink class instead of go.Link.
23 *
24 * If you want to experiment with this extension, try the <a href="../../extensionsJSM/Fishbone.html">Fishbone Layout</a> sample.
25 * @category Layout Extension
26 */
27export class FishboneLayout extends go.TreeLayout {
28 /**
29 * Constructs a FishboneLayout and sets the following properties:
30 * - {@link #alignment} = {@link TreeLayout.AlignmentBusBranching}
31 * - {@link #setsPortSpot} = false
32 * - {@link #setsChildPortSpot} = false
33 */
34 constructor() {
35 super();
36 this.alignment = go.TreeLayout.AlignmentBusBranching;
37 this.setsPortSpot = false;
38 this.setsChildPortSpot = false;
39 }
40
41 /**
42 * Create and initialize a {@link LayoutNetwork} with the given nodes and links.
43 * This override creates dummy vertexes, when necessary, to allow for proper positioning within the fishbone.
44 * @param {Diagram|Group|Iterable.<Part>} coll A {@link Diagram} or a {@link Group} or a collection of {@link Part}s.
45 * @return {LayoutNetwork}
46 */
47 public makeNetwork(coll: go.Diagram | go.Group | go.Iterable<go.Part>): go.LayoutNetwork {
48 // assert(this.angle === 0 || this.angle === 180);
49 // assert(this.alignment === go.TreeLayout.AlignmentBusBranching);
50 // assert(this.path !== go.TreeLayout.PathSource);
51
52 // call base method for standard behavior
53 const net = super.makeNetwork(coll);
54 // make a copy of the collection of TreeVertexes
55 // because we will be modifying the TreeNetwork.vertexes collection in the loop
56 const verts = new go.List<go.TreeVertex>().addAll(net.vertexes.iterator as go.Iterator<go.TreeVertex>);
57 verts.each(function(v: go.TreeVertex) {
58 // ignore leaves of tree
59 if (v.destinationEdges.count === 0) return;
60 if (v.destinationEdges.count % 2 === 1) {
61 // if there's an odd number of real children, add two dummies
62 const dummy = net.createVertex();
63 dummy.bounds = new go.Rect();
64 dummy.focus = new go.Point();
65 net.addVertex(dummy);
66 net.linkVertexes(v, dummy, null);
67 }
68 // make sure there's an odd number of children, including at least one dummy;
69 // commitNodes will move the parent node to where this dummy child node is placed
70 const dummy2 = net.createVertex();
71 dummy2.bounds = v.bounds;
72 dummy2.focus = v.focus;
73 net.addVertex(dummy2);
74 net.linkVertexes(v, dummy2, null);
75 });
76 return net;
77 }
78
79 /**
80 * Add a direction property to each vertex and modify {@link TreeVertex#layerSpacing}.
81 */
82 public assignTreeVertexValues(v: go.TreeVertex): void {
83 super.assignTreeVertexValues(v);
84 (v as any)['_direction'] = 0; // add this property to each TreeVertex
85 if (v.parent !== null) {
86 // The parent node will be moved to where the last dummy will be;
87 // reduce the space to account for the future hole.
88 if (v.angle === 0 || v.angle === 180) {
89 v.layerSpacing -= v.bounds.width;
90 } else {
91 v.layerSpacing -= v.bounds.height;
92 }
93 }
94 }
95
96 /**
97 * Assigns {@link Link#fromSpot}s and {@link Link#toSpot}s based on branching and angle
98 * and moves vertexes based on dummy locations.
99 */
100 public commitNodes(): void {
101 if (this.network === null) return;
102 // vertex Angle is set by BusBranching "inheritance";
103 // assign spots assuming overall Angle === 0 or 180
104 // and links are always connecting horizontal with vertical
105 this.network.edges.each(function(e) {
106 const link = e.link;
107 if (link === null) return;
108 link.fromSpot = go.Spot.None;
109 link.toSpot = go.Spot.None;
110
111 const v: go.TreeVertex = e.fromVertex as go.TreeVertex;
112 const w: go.TreeVertex = e.toVertex as go.TreeVertex;
113
114 if (v.angle === 0) {
115 link.fromSpot = go.Spot.Left;
116 } else if (v.angle === 180) {
117 link.fromSpot = go.Spot.Right;
118 }
119
120 if (w.angle === 0) {
121 link.toSpot = go.Spot.Left;
122 } else if (w.angle === 180) {
123 link.toSpot = go.Spot.Right;
124 }
125 });
126
127 // move the parent node to the location of the last dummy
128 let vit = this.network.vertexes.iterator;
129 while (vit.next()) {
130 const v = vit.value as go.TreeVertex;
131 const len = v.children.length;
132 if (len === 0) continue; // ignore leaf nodes
133 if (v.parent === null) continue; // don't move root node
134 const dummy2 = v.children[len - 1];
135 v.centerX = dummy2.centerX;
136 v.centerY = dummy2.centerY;
137 }
138
139 const layout = this;
140 vit = this.network.vertexes.iterator;
141 while (vit.next()) {
142 const v = vit.value as go.TreeVertex;
143 if (v.parent === null) {
144 layout.shift(v);
145 }
146 }
147
148 // now actually change the Node.location of all nodes
149 super.commitNodes();
150 }
151
152 /**
153 * This override stops links from being committed since the work is done by the {@link FishboneLink} class.
154 */
155 public commitLinks(): void { }
156
157 /**
158 * Shifts subtrees within the fishbone based on angle and node spacing.
159 */
160 public shift(v: go.TreeVertex): void {
161 const p = v.parent;
162 if (p !== null && (v.angle === 90 || v.angle === 270)) {
163 const g = p.parent;
164 if (g !== null) {
165 const shift = v.nodeSpacing;
166 if ((g as any)['_direction'] > 0) {
167 if (g.angle === 90) {
168 if (p.angle === 0) {
169 (v as any)['_direction'] = 1;
170 if (v.angle === 270) this.shiftAll(2, -shift, p, v);
171 } else if (p.angle === 180) {
172 (v as any)['_direction'] = -1;
173 if (v.angle === 90) this.shiftAll(-2, shift, p, v);
174 }
175 } else if (g.angle === 270) {
176 if (p.angle === 0) {
177 (v as any)['_direction'] = 1;
178 if (v.angle === 90) this.shiftAll(2, -shift, p, v);
179 } else if (p.angle === 180) {
180 (v as any)['_direction'] = -1;
181 if (v.angle === 270) this.shiftAll(-2, shift, p, v);
182 }
183 }
184 } else if ((g as any)['_direction'] < 0) {
185 if (g.angle === 90) {
186 if (p.angle === 0) {
187 (v as any)['_direction'] = 1;
188 if (v.angle === 90) this.shiftAll(2, -shift, p, v);
189 } else if (p.angle === 180) {
190 (v as any)['_direction'] = -1;
191 if (v.angle === 270) this.shiftAll(-2, shift, p, v);
192 }
193 } else if (g.angle === 270) {
194 if (p.angle === 0) {
195 (v as any)['_direction'] = 1;
196 if (v.angle === 270) this.shiftAll(2, -shift, p, v);
197 } else if (p.angle === 180) {
198 (v as any)['_direction'] = -1;
199 if (v.angle === 90) this.shiftAll(-2, shift, p, v);
200 }
201 }
202 }
203 } else { // g === null: V is a child of the tree ROOT
204 const dir = ((p.angle === 0) ? 1 : -1);
205 (v as any)['_direction'] = dir;
206 this.shiftAll(dir, 0, p, v);
207 }
208 }
209 for (let i = 0; i < v.children.length; i++) {
210 const c = v.children[i];
211 this.shift(c);
212 }
213 }
214
215 /**
216 * Shifts a subtree.
217 */
218 public shiftAll(direction: number, absolute: number, root: go.TreeVertex, v: go.TreeVertex): void {
219 // assert(root.angle === 0 || root.angle === 180);
220 let locx = v.centerX;
221 locx += direction * Math.abs(root.centerY - v.centerY) / 2;
222 locx += absolute;
223 v.centerX = locx;
224 for (let i = 0; i < v.children.length; i++) {
225 const c = v.children[i];
226 this.shiftAll(direction, absolute, root, c);
227 }
228 }
229 // end FishboneLayout
230}
231
232/**
233 * Custom {@link Link} class for {@link FishboneLayout}.
234 * @category Part Extension
235 */
236export class FishboneLink extends go.Link {
237 public computeAdjusting(): go.EnumValue { return this.adjusting; }
238 /**
239 * Determines the points for this link based on spots and maintains horizontal lines.
240 */
241 public computePoints(): boolean {
242 const result = super.computePoints();
243 if (result) {
244 // insert middle point to maintain horizontal lines
245 if (this.fromSpot.equals(go.Spot.Right) || this.fromSpot.equals(go.Spot.Left)) {
246 let p1: go.Point;
247 // deal with root node being on the "wrong" side
248 const fromnode = this.fromNode;
249 const fromport = this.fromPort;
250 if (fromnode !== null && fromport !== null && fromnode.findLinksInto().count === 0) {
251 // pretend the link is coming from the opposite direction than the declared FromSpot
252 const fromctr = fromport.getDocumentPoint(go.Spot.Center);
253 const fromfar = fromctr.copy();
254 fromfar.x += (this.fromSpot.equals(go.Spot.Left) ? 99999 : -99999);
255 p1 = this.getLinkPointFromPoint(fromnode, fromport, fromctr, fromfar, true).copy();
256 // update the route points
257 this.setPoint(0, p1);
258 let endseg = this.fromEndSegmentLength;
259 if (isNaN(endseg)) endseg = fromport.fromEndSegmentLength;
260 p1.x += (this.fromSpot.equals(go.Spot.Left)) ? endseg : -endseg;
261 this.setPoint(1, p1);
262 } else {
263 p1 = this.getPoint(1); // points 0 & 1 should be OK already
264 }
265 const tonode = this.toNode;
266 const toport = this.toPort;
267 if (tonode !== null && toport !== null) {
268 const toctr = toport.getDocumentPoint(go.Spot.Center);
269 const far = toctr.copy();
270 far.x += (this.fromSpot.equals(go.Spot.Left)) ? -99999 / 2 : 99999 / 2;
271 far.y += (toctr.y < p1.y) ? 99999 : -99999;
272 const p2 = this.getLinkPointFromPoint(tonode, toport, toctr, far, false);
273 this.setPoint(2, p2);
274 let dx = Math.abs(p2.y - p1.y) / 2;
275 if (this.fromSpot.equals(go.Spot.Left)) dx = -dx;
276 this.insertPoint(2, new go.Point(p2.x + dx, p1.y));
277 }
278 } else if (this.toSpot.equals(go.Spot.Right) || this.toSpot.equals(go.Spot.Left)) {
279 const p1: go.Point = this.getPoint(1); // points 1 & 2 should be OK already
280 const fromnode = this.fromNode;
281 const fromport = this.fromPort;
282 if (fromnode !== null && fromport !== null) {
283 const parentlink = fromnode.findLinksInto().first();
284 const fromctr = fromport.getDocumentPoint(go.Spot.Center);
285 const far = fromctr.copy();
286 far.x += (parentlink !== null && parentlink.fromSpot.equals(go.Spot.Left)) ? -99999 / 2 : 99999 / 2;
287 far.y += (fromctr.y < p1.y) ? 99999 : -99999;
288 const p0 = this.getLinkPointFromPoint(fromnode, fromport, fromctr, far, true);
289 this.setPoint(0, p0);
290 let dx = Math.abs(p1.y - p0.y) / 2;
291 if (parentlink !== null && parentlink.fromSpot.equals(go.Spot.Left)) dx = -dx;
292 this.insertPoint(1, new go.Point(p0.x + dx, p1.y));
293 }
294 }
295 }
296 return result;
297 }
298}