1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 | import * as go from '../release/go-module.js';
|
14 |
|
15 |
|
16 |
|
17 |
|
18 |
|
19 |
|
20 |
|
21 |
|
22 |
|
23 |
|
24 |
|
25 |
|
26 |
|
27 |
|
28 |
|
29 |
|
30 |
|
31 |
|
32 | export class DoubleTreeLayout extends go.Layout {
|
33 | private _vertical: boolean = false;
|
34 | private _directionFunction: ((node: go.Node) => boolean) = function(node: go.Node): boolean { return true; };
|
35 | private _bottomRightOptions: Partial<go.TreeLayout> | null = null;
|
36 | private _topLeftOptions: Partial<go.TreeLayout> | null = null;
|
37 |
|
38 | /**
|
39 | * When false, the layout should grow towards the left and towards the right;
|
40 | * when true, the layout show grow upwards and downwards.
|
41 | * The default value is false.
|
42 | */
|
43 | get vertical(): boolean { return this._vertical; }
|
44 | set vertical(value: boolean) {
|
45 | if (typeof value !== "boolean") throw new Error("new value for DoubleTreeLayout.vertical must be a boolean value.");
|
46 | if (this._vertical !== value) {
|
47 | this._vertical = value;
|
48 | this.invalidateLayout();
|
49 | }
|
50 | }
|
51 |
|
52 | /**
|
53 | * This function is called on each child node of the root node
|
54 | * in order to determine whether the subtree starting from that child node
|
55 | * will grow towards larger coordinates or towards smaller ones.
|
56 | * The value must be a function and must not be null.
|
57 | * It must return true if {@link #isPositiveDirection} should return true; otherwise it should return false.
|
58 | */
|
59 | get directionFunction(): ((node: go.Node) => boolean) { return this._directionFunction; }
|
60 | set directionFunction(value: ((node: go.Node) => boolean)) {
|
61 | if (typeof value !== "function") {
|
62 | throw new Error("new value for DoubleTreeLayout.directionFunction must be a function taking a node data object and returning a boolean.");
|
63 | }
|
64 | if (this._directionFunction !== value) {
|
65 | this._directionFunction = value;
|
66 | this.invalidateLayout();
|
67 | }
|
68 | }
|
69 |
|
70 | /**
|
71 | * Gets or sets the options to be applied to a {@link TreeLayout}.
|
72 | * By default this is null -- no properties are set on the TreeLayout
|
73 | * other than the {@link TreeLayout#angle}, depending on {@link #vertical} and
|
74 | * the result of calling {@link #directionFunction}.
|
75 | */
|
76 | get bottomRightOptions(): Partial<go.TreeLayout> | null { return this._bottomRightOptions; }
|
77 | set bottomRightOptions(value: Partial<go.TreeLayout> | null) {
|
78 | if (this._bottomRightOptions !== value) {
|
79 | this._bottomRightOptions = value;
|
80 | this.invalidateLayout();
|
81 | }
|
82 | }
|
83 |
|
84 | /**
|
85 | * Gets or sets the options to be applied to a {@link TreeLayout}.
|
86 | * By default this is null -- no properties are set on the TreeLayout
|
87 | * other than the {@link TreeLayout#angle}, depending on {@link #vertical} and
|
88 | * the result of calling {@link #directionFunction}.
|
89 | */
|
90 | get topLeftOptions(): Partial<go.TreeLayout> | null { return this._topLeftOptions; }
|
91 | set topLeftOptions(value: Partial<go.TreeLayout> | null) {
|
92 | if (this._topLeftOptions !== value) {
|
93 | this._topLeftOptions = value;
|
94 | this.invalidateLayout();
|
95 | }
|
96 | }
|
97 |
|
98 | /**
|
99 | * @ignore
|
100 | * Copies properties to a cloned Layout.
|
101 | */
|
102 | protected cloneProtected(copy: this): void {
|
103 | super.cloneProtected(copy);
|
104 | copy._vertical = this._vertical;
|
105 | copy._directionFunction = this._directionFunction;
|
106 | copy._bottomRightOptions = this._bottomRightOptions;
|
107 | copy._topLeftOptions = this._topLeftOptions;
|
108 | }
|
109 |
|
110 | /**
|
111 | * Perform two {@link TreeLayout}s by splitting the collection of Parts
|
112 | * into two separate subsets but sharing only a single root Node.
|
113 | * @param coll
|
114 | */
|
115 | public doLayout(coll: (go.Diagram | go.Group | go.Iterable<go.Part>)): void {
|
116 | const coll2: go.Set<go.Part> = this.collectParts(coll);
|
117 | if (coll2.count === 0) return;
|
118 | const diagram = this.diagram;
|
119 | if (diagram !== null) diagram.startTransaction("Double Tree Layout");
|
120 |
|
121 | // split the nodes and links into two Sets, depending on direction
|
122 | const leftParts = new go.Set<go.Part>();
|
123 | const rightParts = new go.Set<go.Part>();
|
124 | this.separatePartsForLayout(coll2, leftParts, rightParts);
|
125 | // but the ROOT node will be in both collections
|
126 |
|
127 | // create and perform two TreeLayouts, one in each direction,
|
128 | // without moving the ROOT node, on the different subsets of nodes and links
|
129 | const layout1 = this.createTreeLayout(false);
|
130 | layout1.angle = this.vertical ? 270 : 180;
|
131 | layout1.arrangement = go.TreeLayout.ArrangementFixedRoots;
|
132 |
|
133 | const layout2 = this.createTreeLayout(true);
|
134 | layout2.angle = this.vertical ? 90 : 0;
|
135 | layout2.arrangement = go.TreeLayout.ArrangementFixedRoots;
|
136 |
|
137 | layout1.doLayout(leftParts);
|
138 | layout2.doLayout(rightParts);
|
139 |
|
140 | if (diagram !== null) diagram.commitTransaction("Double Tree Layout");
|
141 | }
|
142 |
|
143 | /**
|
144 | * This just returns an instance of {@link TreeLayout}.
|
145 | * The caller will set the {@link TreeLayout#angle}.
|
146 | * @param {boolean} positive true for growth downward or rightward
|
147 | * @return {TreeLayout}
|
148 | */
|
149 | protected createTreeLayout(positive: boolean): go.TreeLayout {
|
150 | const lay = new go.TreeLayout();
|
151 | let opts = this.topLeftOptions;
|
152 | if (positive) opts = this.bottomRightOptions;
|
153 | if (opts) for (const p in opts) { (<any>lay)[p] = (<any>opts)[p]; }
|
154 | return lay;
|
155 | }
|
156 |
|
157 | /**
|
158 | * This is called by {@link #doLayout} to split the collection of Nodes and Links into two Sets,
|
159 | * one for the subtrees growing towards the left or upwards, and one for the subtrees
|
160 | * growing towards the right or downwards.
|
161 | */
|
162 | protected separatePartsForLayout(coll: go.Set<go.Part>, leftParts: go.Set<go.Part>, rightParts: go.Set<go.Part>): void {
|
163 | let root: go.Node | null = null; // the one root
|
164 | const roots = new go.Set<go.Node>(); // in case there are multiple roots
|
165 | coll.each(function(node: go.Part) {
|
166 | if (node instanceof go.Node && node.findTreeParentNode() === null) roots.add(node);
|
167 | });
|
168 | if (roots.count === 0) { // just choose the first node as the root
|
169 | const it = coll.iterator;
|
170 | while (it.next()) {
|
171 | if (it.value instanceof go.Node) {
|
172 | root = it.value;
|
173 | break;
|
174 | }
|
175 | }
|
176 | } else if (roots.count === 1) { // normal case: just one root node
|
177 | root = roots.first();
|
178 | } else { // multiple root nodes -- create a dummy node to be the one real root
|
179 | root = new go.Node(); // the new root node
|
180 | root.location = new go.Point(0, 0);
|
181 | const forwards = (this.diagram ? this.diagram.isTreePathToChildren : true);
|
182 | // now make dummy links from the one root node to each node
|
183 | roots.each(function(child) {
|
184 | const link = new go.Link();
|
185 | if (forwards) {
|
186 | link.fromNode = root;
|
187 | link.toNode = child;
|
188 | } else {
|
189 | link.fromNode = child;
|
190 | link.toNode = root;
|
191 | }
|
192 | });
|
193 | }
|
194 | if (root === null) return;
|
195 |
|
196 | // the ROOT node is shared by both subtrees
|
197 | leftParts.add(root);
|
198 | rightParts.add(root);
|
199 | const lay = this;
|
200 | // look at all of the immediate children of the ROOT node
|
201 | root.findTreeChildrenNodes().each(function(child) {
|
202 |
|
203 | const bottomright = lay.isPositiveDirection(child);
|
204 | const parts = bottomright ? rightParts : leftParts;
|
205 |
|
206 | parts.addAll(child.findTreeParts());
|
207 |
|
208 | const plink = child.findTreeParentLink();
|
209 | if (plink !== null) parts.add(plink);
|
210 | });
|
211 | }
|
212 |
|
213 | /**
|
214 | * This predicate is called on each child node of the root node,
|
215 | * and only on immediate children of the root.
|
216 | * It should return true if this child node is the root of a subtree that should grow
|
217 | * rightwards or downwards, or false otherwise.
|
218 | * @param {Node} child
|
219 | * @returns {boolean} true if grows towards right or towards bottom; false otherwise
|
220 | */
|
221 | protected isPositiveDirection(child: go.Node): boolean {
|
222 | const f = this.directionFunction;
|
223 | if (!f) throw new Error("No DoubleTreeLayout.directionFunction supplied on the layout");
|
224 | return f(child);
|
225 | }
|
226 | }
|
227 |
|
\ | No newline at end of file |