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 | * Given a root {@link Node}, this arranges connected nodes in concentric rings,
|
14 | * layered by the minimum link distance from the root.
|
15 | *
|
16 | * If you want to experiment with this extension, try the <a href="../../extensionsJSM/Radial.html">Radial Layout</a> sample.
|
17 | * @category Layout Extension
|
18 | */
|
19 | export class RadialLayout extends go.Layout {
|
20 | constructor() {
|
21 | super(...arguments);
|
22 | this._root = null;
|
23 | this._layerThickness = 100; // how thick each ring should be
|
24 | this._maxLayers = Infinity;
|
25 | }
|
26 | /**
|
27 | * Gets or sets the {@link Node} that acts as the root or central node of the radial layout.
|
28 | */
|
29 | get root() { return this._root; }
|
30 | set root(value) {
|
31 | if (this._root !== value) {
|
32 | this._root = value;
|
33 | this.invalidateLayout();
|
34 | }
|
35 | }
|
36 | /**
|
37 | * Gets or sets the thickness of each ring representing a layer.
|
38 | *
|
39 | * The default value is 100.
|
40 | */
|
41 | get layerThickness() { return this._layerThickness; }
|
42 | set layerThickness(value) {
|
43 | if (this._layerThickness !== value) {
|
44 | this._layerThickness = value;
|
45 | this.invalidateLayout();
|
46 | }
|
47 | }
|
48 | /**
|
49 | * Gets or sets the maximum number of layers to be shown, in addition to the root node at layer zero.
|
50 | *
|
51 | * The default value is Infinity.
|
52 | */
|
53 | get maxLayers() { return this._maxLayers; }
|
54 | set maxLayers(value) {
|
55 | if (this._maxLayers !== value) {
|
56 | this._maxLayers = value;
|
57 | this.invalidateLayout();
|
58 | }
|
59 | }
|
60 | /**
|
61 | * Copies properties to a cloned Layout.
|
62 | */
|
63 | cloneProtected(copy) {
|
64 | super.cloneProtected(copy);
|
65 | // don't copy .root
|
66 | copy._layerThickness = this._layerThickness;
|
67 | copy._maxLayers = this._maxLayers;
|
68 | }
|
69 | /**
|
70 | * Use a LayoutNetwork that always creates RadialVertexes.
|
71 | */
|
72 | createNetwork() {
|
73 | const net = new go.LayoutNetwork(this);
|
74 | net.createVertex = () => new RadialVertex(net);
|
75 | return net;
|
76 | }
|
77 | /**
|
78 | * Find distances between root and vertexes, and then lay out radially.
|
79 | * @param {Diagram|Group|Iterable.<Part>} coll A {@link Diagram} or a {@link Group} or a collection of {@link Part}s.
|
80 | */
|
81 | doLayout(coll) {
|
82 | if (this.network === null) {
|
83 | this.network = this.makeNetwork(coll);
|
84 | }
|
85 | if (this.network.vertexes.count === 0) {
|
86 | this.network = null;
|
87 | return;
|
88 | }
|
89 | if (this.root === null) {
|
90 | // If no root supplied, choose one without any incoming edges
|
91 | const rit = this.network.vertexes.iterator;
|
92 | while (rit.next()) {
|
93 | const v = rit.value;
|
94 | if (v.node !== null && v.sourceEdges.count === 0) {
|
95 | this.root = v.node;
|
96 | break;
|
97 | }
|
98 | }
|
99 | }
|
100 | if (this.root === null && this.network !== null) {
|
101 | // If could not find any default root, choose a random one
|
102 | const first = this.network.vertexes.first();
|
103 | this.root = first === null ? null : first.node;
|
104 | }
|
105 | if (this.root === null) { // nothing to do
|
106 | this.network = null;
|
107 | return;
|
108 | }
|
109 | const rootvert = this.network.findVertex(this.root);
|
110 | if (rootvert === null)
|
111 | throw new Error('RadialLayout.root must be a Node in the LayoutNetwork that the RadialLayout is operating on');
|
112 | this.arrangementOrigin = this.initialOrigin(this.arrangementOrigin);
|
113 | this.findDistances(rootvert);
|
114 | // sort all results into Arrays of RadialVertexes with the same distance
|
115 | const verts = [];
|
116 | let maxlayer = 0;
|
117 | const it = this.network.vertexes.iterator;
|
118 | while (it.next()) {
|
119 | const vv = it.value;
|
120 | vv.laid = false;
|
121 | const layer = vv.distance;
|
122 | if (layer === Infinity)
|
123 | continue; // Infinity used as init value (set in findDistances())
|
124 | if (layer > maxlayer)
|
125 | maxlayer = layer;
|
126 | let layerverts = verts[layer];
|
127 | if (layerverts === undefined) {
|
128 | layerverts = [];
|
129 | verts[layer] = layerverts;
|
130 | }
|
131 | layerverts.push(vv);
|
132 | }
|
133 | // now recursively position nodes (using radlay1()), starting with the root
|
134 | rootvert.centerX = this.arrangementOrigin.x;
|
135 | rootvert.centerY = this.arrangementOrigin.y;
|
136 | this.radlay1(rootvert, 1, 0, 360);
|
137 | // Update the "physical" positions of the nodes and links.
|
138 | this.updateParts();
|
139 | this.network = null;
|
140 | }
|
141 | /**
|
142 | * Recursively position vertexes in a radial layout
|
143 | */
|
144 | radlay1(vert, layer, angle, sweep) {
|
145 | if (layer > this.maxLayers)
|
146 | return; // no need to position nodes outside of maxLayers
|
147 | const verts = []; // array of all RadialVertexes connected to 'vert' in layer 'layer'
|
148 | const vit = vert.vertexes.iterator;
|
149 | while (vit.next()) {
|
150 | const v = vit.value;
|
151 | if (v.laid)
|
152 | continue;
|
153 | if (v.distance === layer)
|
154 | verts.push(v);
|
155 | }
|
156 | // vert.vertexes.each((v: go.LayoutVertex) => {
|
157 | // if (!(v instanceof RadialVertex)) return; // typeguard
|
158 | // if (v.laid) return;
|
159 | // if (v.distance === layer) verts.push(v);
|
160 | // });
|
161 | const found = verts.length;
|
162 | if (found === 0)
|
163 | return;
|
164 | const radius = layer * this.layerThickness;
|
165 | const separator = sweep / found; // distance between nodes in their sweep portion
|
166 | const start = angle - sweep / 2 + separator / 2;
|
167 | // for each vertex in this layer, place it in its correct layer and position
|
168 | for (let i = 0; i < found; i++) {
|
169 | const v = verts[i];
|
170 | let a = start + i * separator; // the angle to rotate the node to
|
171 | if (a < 0)
|
172 | a += 360;
|
173 | else if (a > 360)
|
174 | a -= 360;
|
175 | // the point to place the node at -- this corresponds with the layer the node is in
|
176 | // all nodes in the same layer are placed at a constant point, then rotated accordingly
|
177 | const p = new go.Point(radius, 0);
|
178 | p.rotate(a);
|
179 | v.centerX = p.x + this.arrangementOrigin.x;
|
180 | v.centerY = p.y + this.arrangementOrigin.y;
|
181 | v.laid = true;
|
182 | v.angle = a;
|
183 | v.sweep = separator;
|
184 | v.radius = radius;
|
185 | // keep going for all layers
|
186 | this.radlay1(v, layer + 1, a, sweep / found);
|
187 | }
|
188 | }
|
189 | /**
|
190 | * Update RadialVertex.distance for every vertex.
|
191 | */
|
192 | findDistances(source) {
|
193 | if (this.network === null)
|
194 | return;
|
195 | // keep track of distances from the source node
|
196 | const vit = this.network.vertexes.iterator;
|
197 | while (vit.next()) {
|
198 | const v = vit.value;
|
199 | v.distance = Infinity;
|
200 | }
|
201 | // this.network.vertexes.each((v: go.LayoutVertex) => {
|
202 | // if (!(v instanceof RadialVertex)) return; // typeguard
|
203 | // v.distance = Infinity;
|
204 | // });
|
205 | // the source node starts with distance 0
|
206 | source.distance = 0;
|
207 | // keep track of nodes for we have set a non-Infinity distance,
|
208 | // but which we have not yet finished examining
|
209 | const seen = new go.Set();
|
210 | seen.add(source);
|
211 | // local function for finding a vertex with the smallest distance in a given collection
|
212 | function leastVertex(coll) {
|
213 | let bestdist = Infinity;
|
214 | let bestvert = null;
|
215 | const it = coll.iterator;
|
216 | while (it.next()) {
|
217 | const v = it.value;
|
218 | const dist = v.distance;
|
219 | if (dist < bestdist) {
|
220 | bestdist = dist;
|
221 | bestvert = v;
|
222 | }
|
223 | }
|
224 | return bestvert;
|
225 | }
|
226 | // keep track of vertexes we have finished examining;
|
227 | // this avoids unnecessary traversals and helps keep the SEEN collection small
|
228 | const finished = new go.Set();
|
229 | while (seen.count > 0) {
|
230 | // look at the unfinished vertex with the shortest distance so far
|
231 | const least = leastVertex(seen);
|
232 | if (least === null)
|
233 | return;
|
234 | const leastdist = least.distance;
|
235 | // by the end of this loop we will have finished examining this LEAST vertex
|
236 | seen.remove(least);
|
237 | finished.add(least);
|
238 | // look at all edges connected with this vertex
|
239 | least.edges.each(function (e) {
|
240 | if (least === null)
|
241 | return;
|
242 | const neighbor = e.getOtherVertex(least);
|
243 | // skip vertexes that we have finished
|
244 | if (finished.contains(neighbor))
|
245 | return;
|
246 | const neighbordist = neighbor.distance;
|
247 | // assume "distance" along a link is unitary, but could be any non-negative number.
|
248 | const dist = leastdist + 1;
|
249 | if (dist < neighbordist) {
|
250 | // if haven't seen that vertex before, add it to the SEEN collection
|
251 | if (neighbordist === Infinity) {
|
252 | seen.add(neighbor);
|
253 | }
|
254 | // record the new best distance so far to that node
|
255 | neighbor.distance = dist;
|
256 | }
|
257 | });
|
258 | }
|
259 | }
|
260 | /**
|
261 | * This override positions each Node and also calls {@link #rotateNode}.
|
262 | */
|
263 | commitLayout() {
|
264 | super.commitLayout();
|
265 | if (this.network !== null) {
|
266 | const it = this.network.vertexes.iterator;
|
267 | while (it.next()) {
|
268 | const v = it.value;
|
269 | const n = v.node;
|
270 | if (n !== null) {
|
271 | n.visible = (v.distance <= this.maxLayers);
|
272 | this.rotateNode(n, v.angle, v.sweep, v.radius);
|
273 | }
|
274 | }
|
275 | }
|
276 | this.commitLayers();
|
277 | }
|
278 | /**
|
279 | * Override this method in order to modify each node as it is laid out.
|
280 | * By default this method does nothing.
|
281 | * @expose
|
282 | */
|
283 | rotateNode(node, angle, sweep, radius) { }
|
284 | /**
|
285 | * Override this method in order to create background circles indicating the layers of the radial layout.
|
286 | * By default this method does nothing.
|
287 | * @expose
|
288 | */
|
289 | commitLayers() { }
|
290 | } // end RadialLayout
|
291 | /**
|
292 | * RadialVertex, a LayoutVertex that holds additional info
|
293 | */
|
294 | class RadialVertex extends go.LayoutVertex {
|
295 | constructor(network) {
|
296 | super(network);
|
297 | this.distance = Infinity; // number of layers from the root, non-negative integers
|
298 | this.laid = false; // used internally to keep track
|
299 | this.angle = 0; // the direction at which the node is placed relative to the root node
|
300 | this.sweep = 0; // the angle subtended by the vertex
|
301 | this.radius = 0; // the inner radius of the layer containing this vertex
|
302 | }
|
303 | }
|