UNPKG

17.2 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* A custom Layout that provides one way to have a layout of layouts.
14* It partitions nodes and links into separate subgraphs, applies a primary
15* layout to each subgraph, and then arranges those results by an
16* arranging layout. Any disconnected nodes are laid out later by a
17* side layout, by default in a grid underneath the main body of subgraphs.
18*
19* If you want to experiment with this extension, try the <a href="../../extensionsJSM/Arranging.html">Arranging Layout</a> sample.
20*
21* This layout uses three separate Layouts.
22*
23* One is used for laying out nodes and links that are connected together: {@link #primaryLayout}.
24* This defaults to null and must be set to an instance of a {@link Layout},
25* such as a {@link TreeLayout} or a {@link ForceDirectedLayout} or a custom Layout.
26*
27* One is used to arrange separate subnetworks of the main graph: {@link #arrangingLayout}.
28* This defaults to an instance of {@link GridLayout}.
29*
30* One is used for laying out the additional nodes along one of the sides of the main graph: {@link #sideLayout}.
31* This also defaults to an instance of {@link GridLayout}.
32* A filter predicate, {@link #filter}, splits up the collection of nodes and links into two subsets,
33* one for the main layout and one for the side layout.
34* By default, when there is no filter, it puts all nodes that have no link connections into the
35* subset to be processed by the side layout.
36*
37* If all pairs of nodes in the main graph can be reached by some path of undirected links,
38* there are no separate subnetworks, so the {@link #arrangingLayout} need not be used and
39* the {@link #primaryLayout} would apply to all of those nodes and links.
40*
41* But if there are disconnected subnetworks, the {@link #primaryLayout} is applied to each subnetwork,
42* and then all of those results are arranged by the {@link #arrangingLayout}.
43*
44* In either case if there are any nodes in the side graph, those are arranged by the {@link #sideLayout}
45* to be on the side of the arrangement of the main graph of nodes and links.
46*
47* Note: if you do not want to have singleton nodes be arranged by {@link #sideLayout},
48* set {@link #filter} to <code>function(part) { return true; }</code>.
49* That will cause all singleton nodes to be arranged by {@link #arrangingLayout} as if they
50* were each their own subgraph.
51*
52* If you both don't want to use {@link #sideLayout} and you don't want to use {@link #arrangingLayout}
53* to lay out connected subgraphs, don't use this ArrangingLayout at all --
54* just use whatever Layout you would have assigned to {@link #primaryLayout}.
55*
56* @category Layout Extension
57*/
58export class ArrangingLayout extends go.Layout {
59 constructor() {
60 super();
61 this._filter = null;
62 this._side = go.Spot.BottomSide;
63 this._spacing = new go.Size(20, 20);
64 const play = new go.GridLayout();
65 play.cellSize = new go.Size(1, 1);
66 this._primaryLayout = play;
67 const alay = new go.GridLayout();
68 alay.cellSize = new go.Size(1, 1);
69 this._arrangingLayout = alay;
70 const slay = new go.GridLayout();
71 slay.cellSize = new go.Size(1, 1);
72 this._sideLayout = slay;
73 }
74 /**
75 * @ignore @hidden @internal
76 * Copies properties to a cloned Layout.
77 */
78 cloneProtected(copy) {
79 super.cloneProtected(copy);
80 copy._filter = this._filter;
81 if (this._primaryLayout !== null)
82 copy._primaryLayout = this._primaryLayout.copy();
83 if (this._arrangingLayout !== null)
84 copy._arrangingLayout = this._arrangingLayout.copy();
85 if (this._sideLayout !== null)
86 copy._sideLayout = this._sideLayout.copy();
87 copy._side = this._side.copy();
88 copy._spacing = this._spacing.copy();
89 }
90 ;
91 /**
92 * @hidden @internal
93 * @param {Diagram|Group|Iterable} coll the collection of Parts to layout.
94 */
95 doLayout(coll) {
96 const coll2 = this.collectParts(coll);
97 const diagram = this.diagram;
98 if (diagram === null)
99 throw new Error("No Diagram for this Layout");
100 // implementations of doLayout that do not make use of a LayoutNetwork
101 // need to perform their own transactions
102 diagram.startTransaction("Arranging Layout");
103 const maincoll = new go.Set();
104 const sidecoll = new go.Set();
105 this.splitParts(coll2, maincoll, sidecoll);
106 let mainnet = null;
107 let subnets = null;
108 if (this.arrangingLayout !== null) {
109 mainnet = this.makeNetwork(maincoll);
110 subnets = mainnet.splitIntoSubNetworks();
111 }
112 let bounds = null;
113 if (this.arrangingLayout !== null && mainnet !== null && subnets !== null && subnets.count > 1) {
114 const groups = new go.Map();
115 const it = subnets.iterator;
116 while (it.next()) {
117 const net = it.value;
118 const subcoll = net.findAllParts();
119 this.preparePrimaryLayout(this.primaryLayout, subcoll);
120 this.primaryLayout.doLayout(subcoll);
121 this._addMainNode(groups, subcoll, diagram);
122 }
123 const mit = mainnet.vertexes.iterator;
124 while (mit.next()) {
125 const v = mit.value;
126 if (v.node) {
127 const subcoll = new go.Set();
128 subcoll.add(v.node);
129 this.preparePrimaryLayout(this.primaryLayout, subcoll);
130 this.primaryLayout.doLayout(subcoll);
131 this._addMainNode(groups, subcoll, diagram);
132 }
133 }
134 this.arrangingLayout.doLayout(groups.toKeySet());
135 const git = groups.iterator;
136 while (git.next()) {
137 const grp = git.key;
138 const ginfo = git.value;
139 this.moveSubgraph(ginfo.parts, ginfo.bounds, new go.Rect(grp.position, grp.desiredSize));
140 }
141 bounds = diagram.computePartsBounds(groups.toKeySet()); // not maincoll due to links without real bounds
142 }
143 else { // no this.arrangingLayout
144 this.preparePrimaryLayout(this.primaryLayout, maincoll);
145 this.primaryLayout.doLayout(maincoll);
146 bounds = diagram.computePartsBounds(maincoll);
147 this.moveSubgraph(maincoll, bounds, bounds);
148 }
149 if (!bounds.isReal())
150 bounds = new go.Rect(0, 0, 0, 0);
151 this.prepareSideLayout(this.sideLayout, sidecoll, bounds);
152 if (sidecoll.count > 0) {
153 this.sideLayout.doLayout(sidecoll);
154 let sidebounds = diagram.computePartsBounds(sidecoll);
155 if (!sidebounds.isReal())
156 sidebounds = new go.Rect(0, 0, 0, 0);
157 this.moveSideCollection(sidecoll, bounds, sidebounds);
158 }
159 diagram.commitTransaction("Arranging Layout");
160 }
161 ;
162 /**
163 * @hidden @internal
164 * @param {*} subcoll
165 */
166 _addMainNode(groups, subcoll, diagram) {
167 const grp = new go.Node();
168 grp.locationSpot = go.Spot.Center;
169 const grpb = diagram.computePartsBounds(subcoll);
170 grp.desiredSize = grpb.size;
171 grp.position = grpb.position;
172 groups.add(grp, { parts: subcoll, bounds: grpb });
173 }
174 /**
175 * Assign all of the Parts in the given collection into either the
176 * set of Nodes and Links for the main graph or the set of Nodes and Links
177 * for the side graph.
178 *
179 * By default this just calls the {@link #filter} on each non-Link to decide,
180 * and then looks at each Link's connected Nodes to decide.
181 *
182 * A null filter assigns all Nodes that have connected Links to the main graph, and
183 * all Links will be assigned to the main graph, and the side graph will only contain
184 * Parts with no connected Links.
185 * @param {Set} coll
186 * @param {Set} maincoll
187 * @param {Set} sidecoll
188 */
189 splitParts(coll, maincoll, sidecoll) {
190 // first consider all Nodes
191 const pred = this.filter;
192 coll.each(function (p) {
193 if (p instanceof go.Link)
194 return;
195 let main;
196 if (pred)
197 main = pred(p);
198 else if (p instanceof go.Node)
199 main = (p.linksConnected.count > 0);
200 else
201 main = (p instanceof go.Link);
202 if (main) {
203 maincoll.add(p);
204 }
205 else {
206 sidecoll.add(p);
207 }
208 });
209 // now assign Links based on which Nodes they connect with
210 coll.each(function (p) {
211 if (p instanceof go.Link) {
212 if (!p.fromNode || !p.toNode)
213 return;
214 if (maincoll.contains(p.fromNode) && maincoll.contains(p.toNode)) {
215 maincoll.add(p);
216 }
217 else if (sidecoll.contains(p.fromNode) && sidecoll.contains(p.toNode)) {
218 sidecoll.add(p);
219 }
220 }
221 });
222 }
223 /**
224 * This method is called just before the primaryLayout is performed so that
225 * there can be adjustments made to the primaryLayout, if desired.
226 * By default this method makes no adjustments to the primaryLayout.
227 * @param {Layout} primaryLayout the sideLayout that may be modified for the results of the primaryLayout
228 * @param {Set} mainColl the Nodes and Links to be laid out by primaryLayout after being separated into subnetworks
229 */
230 preparePrimaryLayout(primaryLayout, mainColl) {
231 // by default this is a no-op
232 }
233 /**
234 * Move a Set of Nodes and Links to the given area.
235 * @param {Set} subColl the Set of Nodes and Links that form a separate connected subgraph
236 * @param {Rect} subbounds the area occupied by the subColl
237 * @param {Rect} bounds the area where they should be moved according to the arrangingLayout
238 */
239 moveSubgraph(subColl, subbounds, bounds) {
240 const diagram = this.diagram;
241 if (!diagram)
242 return;
243 diagram.moveParts(subColl, bounds.position.subtract(subbounds.position), false);
244 }
245 /**
246 * This method is called just after the main layouts (the primaryLayouts and arrangingLayout)
247 * have been performed and just before the sideLayout is performed so that there can be
248 * adjustments made to the sideLayout, if desired.
249 * By default this method makes no adjustments to the sideLayout.
250 * @param {Layout} sideLayout the sideLayout that may be modified for the results of the main layouts
251 * @param {Set} sideColl the Nodes and Links filtered out to be laid out by sideLayout
252 * @param {Rect} mainBounds the area occupied by the nodes and links of the main layout, after it was performed
253 */
254 prepareSideLayout(sideLayout, sideColl, mainBounds) {
255 // by default this is a no-op
256 }
257 /**
258 * This method is called just after the sideLayout has been performed in order to move
259 * its parts to the desired area relative to the results of the main layouts.
260 * By default this calls {@link Diagram#moveParts} on the sidecoll collection to the {@link #side} of the mainbounds.
261 * This won't get called if there are no Parts in the sidecoll collection.
262 * @param {Set} sidecoll a collection of Parts that were laid out by the sideLayout
263 * @param {Rect} mainbounds the area occupied by the results of the main layouts
264 * @param {Rect} sidebounds the area occupied by the results of the sideLayout
265 */
266 moveSideCollection(sidecoll, mainbounds, sidebounds) {
267 const diagram = this.diagram;
268 if (!diagram)
269 return;
270 if (this.side.includesSide(go.Spot.BottomSide)) {
271 diagram.moveParts(sidecoll, new go.Point(mainbounds.x - sidebounds.x, mainbounds.y + mainbounds.height + this.spacing.height - sidebounds.y), false);
272 }
273 else if (this.side.includesSide(go.Spot.RightSide)) {
274 diagram.moveParts(sidecoll, new go.Point(mainbounds.x + mainbounds.width + this.spacing.width - sidebounds.x, mainbounds.y - sidebounds.y), false);
275 }
276 else if (this.side.includesSide(go.Spot.TopSide)) {
277 diagram.moveParts(sidecoll, new go.Point(mainbounds.x - sidebounds.x, mainbounds.y - sidebounds.height - this.spacing.height - sidebounds.y), false);
278 }
279 else if (this.side.includesSide(go.Spot.LeftSide)) {
280 diagram.moveParts(sidecoll, new go.Point(mainbounds.x - sidebounds.width - this.spacing.width - sidebounds.x, mainbounds.y - sidebounds.y), false);
281 }
282 }
283 // Public properties
284 /**
285 * Gets or sets the predicate function to call on each non-Link.
286 * If the predicate returns true, the part will be laid out by the main layouts,
287 * the primaryLayouts and the arrangingLayout, otherwise by the sideLayout.
288 * The default value is a function that is true when there are any links connecting with the node.
289 * Such default behavior will have the sideLayout position all of the singleton nodes.
290 */
291 get filter() { return this._filter; }
292 set filter(val) {
293 if (val && typeof val !== 'function')
294 throw new Error("new value for ArrangingLayout.filter must be a function, not: " + val);
295 if (this._filter !== val) {
296 this._filter = val;
297 this.invalidateLayout();
298 }
299 }
300 /**
301 * Gets or sets the side {@link Spot} where the side nodes and links should be laid out,
302 * relative to the results of the main Layout.
303 * The default value is Spot.BottomSide.
304 * Currently only handles a single side.
305 * @name ArrangingLayout#side
306
307 * @return {Spot}
308 */
309 get side() { return this._side; }
310 set side(val) {
311 if (!(val instanceof go.Spot) || !val.isSide()) {
312 throw new Error("new value for ArrangingLayout.side must be a side Spot, not: " + val);
313 }
314 if (!this._side.equals(val)) {
315 this._side = val.copy();
316 this.invalidateLayout();
317 }
318 }
319 /**
320 * Gets or sets the space between the main layout and the side layout.
321 * The default value is Size(20, 20).
322 * @name ArrangingLayout#spacing
323
324 * @return {Size}
325 */
326 get spacing() { return this._spacing; }
327 set spacing(val) {
328 if (!(val instanceof go.Size))
329 throw new Error("new value for ArrangingLayout.spacing must be a Size, not: " + val);
330 if (!this._spacing.equals(val)) {
331 this._spacing = val.copy();
332 this.invalidateLayout();
333 }
334 }
335 /**
336 * Gets or sets the Layout used for the main part of the diagram.
337 * The default value is an instance of GridLayout.
338 * Any new value must not be null.
339 */
340 get primaryLayout() { return this._primaryLayout; }
341 set primaryLayout(val) {
342 if (!(val instanceof go.Layout))
343 throw new Error("layout does not inherit from go.Layout: " + val);
344 this._primaryLayout = val;
345 this.invalidateLayout();
346 }
347 /**
348 * Gets or sets the Layout used to arrange multiple separate connected subgraphs of the main graph.
349 * The default value is an instance of GridLayout.
350 * Set this property to null in order to get the default behavior of the @{link #primaryLayout}
351 * when dealing with multiple connected graphs as a whole.
352 */
353 get arrangingLayout() { return this._arrangingLayout; }
354 set arrangingLayout(val) {
355 if (val && !(val instanceof go.Layout))
356 throw new Error("layout does not inherit from go.Layout: " + val);
357 this._arrangingLayout = val;
358 this.invalidateLayout();
359 }
360 /**
361 * Gets or sets the Layout used to arrange the "side" nodes and links -- those outside of the main layout.
362 * The default value is an instance of GridLayout.
363 * Any new value must not be null.
364 */
365 get sideLayout() { return this._sideLayout; }
366 set sideLayout(val) {
367 if (!(val instanceof go.Layout))
368 throw new Error("layout does not inherit from go.Layout: " + val);
369 this._sideLayout = val;
370 this.invalidateLayout();
371 }
372}