UNPKG

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