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