-
Notifications
You must be signed in to change notification settings - Fork 2.9k
/
Copy pathDoubleTreeLayout.js
239 lines (238 loc) · 9.58 KB
/
DoubleTreeLayout.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
/*
* Copyright 1998-2025 by Northwoods Software Corporation. All Rights Reserved.
*/
/*
* This is an extension and not part of the main GoJS library.
* The source code for this is at extensionsJSM/DoubleTreeLayout.ts.
* Note that the API for this class may change with any version, even point releases.
* If you intend to use an extension in production, you should copy the code to your own source directory.
* Extensions can be found in the GoJS kit under the extensions or extensionsJSM folders.
* See the Extensions intro page (https://gojs.net/latest/intro/extensions.html) for more information.
*/
/**
* Perform two TreeLayouts, one going rightwards and one going leftwards.
* The choice of direction is determined by the mandatory predicate {@link directionFunction},
* which is called on each child Node of the root Node.
*
* You can also set {@link vertical} to true if you want the DoubleTreeLayout to
* perform TreeLayouts both downwards and upwards.
*
* Normally there should be a single root node. Hoewver if there are multiple root nodes
* found in the nodes and links that this layout is responsible for, this will pretend that
* there is a real root node and make all of the apparent root nodes children of that pretend root.
*
* If there is no root node, all nodes are involved in cycles, so the first given node is chosen.
*
* If you want to experiment with this extension, try the <a href="../../samples/doubleTree.html">Double Tree</a> sample.
* @category Layout Extension
*/
class DoubleTreeLayout extends go.Layout {
constructor(init) {
super();
this._vertical = false;
this._directionFunction = (node) => true;
this._bottomRightOptions = null;
this._topLeftOptions = null;
if (init)
Object.assign(this, init);
}
/**
* When false, the layout should grow towards the left and towards the right;
* when true, the layout show grow upwards and downwards.
* The default value is false.
*/
get vertical() {
return this._vertical;
}
set vertical(value) {
if (this._vertical !== value) {
if (typeof value !== 'boolean')
throw new Error('new value for DoubleTreeLayout.vertical must be a boolean value.');
this._vertical = value;
this.invalidateLayout();
}
}
/**
* This function is called on each child node of the root node
* in order to determine whether the subtree starting from that child node
* will grow towards larger coordinates or towards smaller ones.
* The value must be a function and must not be null.
* It must return true if {@link isPositiveDirection} should return true; otherwise it should return false.
*/
get directionFunction() {
return this._directionFunction;
}
set directionFunction(value) {
if (this._directionFunction !== value) {
if (typeof value !== 'function') {
throw new Error('new value for DoubleTreeLayout.directionFunction must be a function taking a node data object and returning a boolean.');
}
this._directionFunction = value;
this.invalidateLayout();
}
}
/**
* Gets or sets the options to be applied to a {@link go.TreeLayout}.
* By default this is null -- no properties are set on the TreeLayout
* other than the {@link go.TreeLayout.angle}, depending on {@link vertical} and
* the result of calling {@link directionFunction}.
*/
get bottomRightOptions() {
return this._bottomRightOptions;
}
set bottomRightOptions(value) {
if (this._bottomRightOptions !== value) {
this._bottomRightOptions = value;
this.invalidateLayout();
}
}
/**
* Gets or sets the options to be applied to a {@link go.TreeLayout}.
* By default this is null -- no properties are set on the TreeLayout
* other than the {@link go.TreeLayout.angle}, depending on {@link vertical} and
* the result of calling {@link directionFunction}.
*/
get topLeftOptions() {
return this._topLeftOptions;
}
set topLeftOptions(value) {
if (this._topLeftOptions !== value) {
this._topLeftOptions = value;
this.invalidateLayout();
}
}
/**
* @hidden @internal
* Copies properties to a cloned Layout.
*/
cloneProtected(copy) {
super.cloneProtected(copy);
copy._vertical = this._vertical;
copy._directionFunction = this._directionFunction;
copy._bottomRightOptions = this._bottomRightOptions;
copy._topLeftOptions = this._topLeftOptions;
}
/**
* Perform two {@link go.TreeLayout}s by splitting the collection of Parts
* into two separate subsets but sharing only a single root Node.
* @param coll
*/
doLayout(coll) {
const coll2 = this.collectParts(coll);
if (coll2.count === 0)
return;
const diagram = this.diagram;
if (diagram !== null)
diagram.startTransaction('Double Tree Layout');
// split the nodes and links into two Sets, depending on direction
const leftParts = new go.Set();
const rightParts = new go.Set();
this.separatePartsForLayout(coll2, leftParts, rightParts);
// but the ROOT node will be in both collections
// create and perform two TreeLayouts, one in each direction,
// without moving the ROOT node, on the different subsets of nodes and links
const layout1 = this.createTreeLayout(false);
layout1.diagram = diagram;
layout1.angle = this.vertical ? 270 : 180;
layout1.arrangement = go.TreeArrangement.FixedRoots;
const layout2 = this.createTreeLayout(true);
layout2.diagram = diagram;
layout2.angle = this.vertical ? 90 : 0;
layout2.arrangement = go.TreeArrangement.FixedRoots;
layout1.doLayout(leftParts);
layout2.doLayout(rightParts);
if (diagram !== null)
diagram.commitTransaction('Double Tree Layout');
}
/**
* This just returns an instance of {@link go.TreeLayout}.
* The caller will set the {@link go.TreeLayout.angle}.
* @param positive - true for growth downward or rightward
*/
createTreeLayout(positive) {
const lay = new go.TreeLayout();
let opts = this.topLeftOptions;
if (positive)
opts = this.bottomRightOptions;
if (opts)
Object.assign(lay, opts);
return lay;
}
/**
* This is called by {@link doLayout} to split the collection of Nodes and Links into two Sets,
* one for the subtrees growing towards the left or upwards, and one for the subtrees
* growing towards the right or downwards.
*/
separatePartsForLayout(coll, leftParts, rightParts) {
let root = null; // the one root
const roots = new go.Set(); // in case there are multiple roots
coll.each((node) => {
if (node instanceof go.Node && node.findTreeParentNode() === null)
roots.add(node);
});
if (roots.count === 0) {
// just choose the first node as the root
const it = coll.iterator;
while (it.next()) {
if (it.value instanceof go.Node) {
root = it.value;
break;
}
}
}
else if (roots.count === 1) {
// normal case: just one root node
root = roots.first();
}
else {
// multiple root nodes -- create a dummy node to be the one real root
root = new go.Node(); // the new root node
root.location = new go.Point(0, 0);
const forwards = this.diagram ? this.diagram.isTreePathToChildren : true;
// now make dummy links from the one root node to each node
roots.each((child) => {
const link = new go.Link();
if (forwards) {
link.fromNode = root;
link.toNode = child;
}
else {
link.fromNode = child;
link.toNode = root;
}
});
}
if (root === null)
return;
// the ROOT node is shared by both subtrees
leftParts.add(root);
rightParts.add(root);
const lay = this;
// look at all of the immediate children of the ROOT node
root.findTreeChildrenNodes().each((child) => {
// in what direction is this child growing?
const bottomright = lay.isPositiveDirection(child);
const parts = bottomright ? rightParts : leftParts;
// add the whole subtree starting with this child node
parts.addAll(child.findTreeParts());
// and also add the link from the ROOT node to this child node
const plink = child.findTreeParentLink();
if (plink !== null)
parts.add(plink);
});
}
/**
* This predicate is called on each child node of the root node,
* and only on immediate children of the root.
* It should return true if this child node is the root of a subtree that should grow
* rightwards or downwards, or false otherwise.
* @param child
* @returns {boolean} true if grows towards right or towards bottom; false otherwise
*/
isPositiveDirection(child) {
const f = this.directionFunction;
if (!f)
throw new Error('No DoubleTreeLayout.directionFunction supplied on the layout');
return f(child);
}
}