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