UNPKG

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