UNPKG

21.4 kBJavaScriptView Raw
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*/
11import * as go from '../release/go-module.js';
12/**
13 * A custom LayeredDigraphLayout that knows about "lanes"
14 * and that positions each node in its respective lane.
15
16 * This assumes that each Node.data.lane property is a string that names the lane the node should be in.
17 * You can set the {@link #laneProperty} property to use a different data property name.
18 * It is commonplace to set this property to be the same as the {@link GraphLinksModel#nodeGroupKeyProperty},
19 * so that the one property indicates that a particular node data is a member of a particular group
20 * and thus that that group represents a lane.
21
22 * The lanes can be sorted by specifying the {@link #laneComparer} function.
23
24 * You can add extra space between the lanes by increasing {@link #laneSpacing} from its default of zero.
25 * That number's unit is columns, {@link LayeredDigraphLayout#columnSpacing}, not in document coordinates.
26 * @category Layout Extension
27 */
28export class SwimLaneLayout extends go.LayeredDigraphLayout {
29 constructor() {
30 super(...arguments);
31 // settable properties
32 this._laneProperty = "lane"; // how to get lane identifier string from node data
33 this._laneNames = []; // lane names, may be sorted using this.laneComparer
34 this._laneComparer = null;
35 this._laneSpacing = 0; // in columns
36 this._router = { linkSpacing: 4 };
37 this._reducer = null;
38 // computed, read-only state
39 this._lanePositions = new go.Map(); // lane names --> start columns, left to right
40 this._laneBreadths = new go.Map(); // lane names --> needed width in columns
41 // internal state
42 this._layers = [[]];
43 this._neededSpaces = [];
44 }
45 /**
46 * Gets or sets the name of the data property that holds the string which is the name of the lane that the node should be in.
47 * The default value is "lane".
48 */
49 get laneProperty() { return this._laneProperty; }
50 set laneProperty(val) {
51 if (typeof val !== 'string' && typeof val !== 'function')
52 throw new Error("new value for SwimLaneLayout.laneProperty must be a property name, not: " + val);
53 if (this._laneProperty !== val) {
54 this._laneProperty = val;
55 this.invalidateLayout();
56 }
57 }
58 /**
59 * Gets or sets an Array of lane names.
60 * If you set this before a layout happens, it will use those lanes in that order.
61 * Any additional lane names that it discovers will be added to the end of this Array.
62 *
63 * This property is reset to an empty Array at the end of each layout.
64 * The default value is an empty Array.
65 */
66 get laneNames() { return this._laneNames; }
67 set laneNames(val) {
68 if (!Array.isArray(val))
69 throw new Error("new value for SwimLaneLayout.laneNames must be an Array, not: " + val);
70 if (this._laneNames !== val) {
71 this._laneNames = val;
72 this.invalidateLayout();
73 }
74 }
75 /**
76 * Gets or sets a function by which to compare lane names, for ordering the lanes within the {@link #laneNames} Array.
77 * By default the function is null -- the lanes are not sorted.
78 */
79 get laneComparer() { return this._laneComparer; }
80 set laneComparer(val) {
81 if (typeof val !== 'function')
82 throw new Error("new value for SwimLaneLayout.laneComparer must be a function, not: " + val);
83 if (this._laneComparer !== val) {
84 this._laneComparer = val;
85 this.invalidateLayout();
86 }
87 }
88 /**
89 * Gets or sets the amount of additional space it allocates between the lanes.
90 * This number specifies the number of columns, with the same meaning as {@link LayeredDigraphLayout#columnSpacing}.
91 * The number unit is not in document coordinate or pixels.
92 * The default value is zero columns.
93 */
94 get laneSpacing() { return this._laneSpacing; }
95 set laneSpacing(val) {
96 if (typeof val !== 'number')
97 throw new Error("new value for SwimLaneLayout.laneSpacing must be a number, not: " + val);
98 if (this._laneSpacing !== val) {
99 this._laneSpacing = val;
100 this.invalidateLayout();
101 }
102 }
103 /**
104 * @hidden
105 */
106 get router() { return this._router; }
107 set router(val) {
108 if (this._router !== val) {
109 this._router = val;
110 this.invalidateLayout();
111 }
112 }
113 /**
114 * @hidden
115 */
116 get reducer() { return this._reducer; }
117 set reducer(val) {
118 if (this._reducer !== val) {
119 this._reducer = val;
120 if (val) {
121 const lay = this;
122 val.findLane = function (v) { return lay.getLane(v); };
123 val.getIndex = function (v) { return v.index; };
124 val.getBary = function (v) { return v.bary || 0; };
125 val.setBary = function (v, val) { v.bary = val; };
126 val.getConnectedNodesIterator = function (v) { return v.vertexes; };
127 }
128 this.invalidateLayout();
129 }
130 }
131 /**
132 * The computed positions of each lane,
133 * in the form of a {@link Map} mapping lane names (strings) to numbers.
134 */
135 get lanePositions() { return this._lanePositions; }
136 /**
137 * The computed breadths (widths or heights depending on the direction) of each lane,
138 * in the form of a {@link Map} mapping lane names (strings) to numbers.
139 */
140 get laneBreadths() { return this._laneBreadths; }
141 /**
142 * @hidden
143 * @param coll
144 */
145 doLayout(coll) {
146 this.lanePositions.clear(); // lane names --> start columns, left to right
147 this.laneBreadths.clear(); // lane names --> needed width in columns
148 this._layers = [[]];
149 this._neededSpaces = [];
150 super.doLayout(coll);
151 this.lanePositions.clear();
152 this.laneBreadths.clear();
153 this._layers = [[]];
154 this._neededSpaces = [];
155 this.laneNames = []; // clear out for next layout
156 }
157 /**
158 * @hidden
159 * @param v
160 * @param topleft
161 */
162 nodeMinLayerSpace(v, topleft) {
163 if (!this._neededSpaces)
164 this._neededSpaces = this.computeNeededLayerSpaces(this.network);
165 if (v.node === null)
166 return 0;
167 let lay = v.layer;
168 if (!topleft) {
169 if (lay > 0)
170 lay--;
171 }
172 const overlaps = (this._neededSpaces[lay] || 0) / 2;
173 const edges = this.countEdgesForDirection(v, (this.direction > 135) ? !topleft : topleft);
174 const needed = Math.max(overlaps, edges) * this.router.linkSpacing * 1.5;
175 if (this.direction === 90 || this.direction === 270) {
176 if (topleft) {
177 return v.focus.y + 10 + needed;
178 }
179 else {
180 return v.bounds.height - v.focus.y + 10 + needed;
181 }
182 }
183 else {
184 if (topleft) {
185 return v.focus.x + 10 + needed;
186 }
187 else {
188 return v.bounds.width - v.focus.x + 10 + needed;
189 }
190 }
191 }
192 countEdgesForDirection(vertex, topleft) {
193 let c = 0;
194 const lay = vertex.layer;
195 vertex.edges.each(function (e) {
196 if (topleft) {
197 if (e.getOtherVertex(vertex).layer >= lay)
198 c++;
199 }
200 else {
201 if (e.getOtherVertex(vertex).layer <= lay)
202 c++;
203 }
204 });
205 return c;
206 }
207 computeNeededLayerSpaces(net) {
208 // group all edges by their connected vertexes' least layer
209 const layerMinEdges = [];
210 net.edges.each(function (e) {
211 // consider all edges, including dummy ones!
212 const f = e.fromVertex;
213 const t = e.toVertex;
214 if (f.column === t.column)
215 return; // skip edges that don't go between columns
216 if (Math.abs(f.layer - t.layer) > 1)
217 return; // skip edges that don't go between adjacent layers
218 const lay = Math.min(f.layer, t.layer);
219 let arr = layerMinEdges[lay];
220 if (!arr)
221 arr = layerMinEdges[lay] = [];
222 arr.push(e);
223 });
224 // sort each array of edges by their lowest connected vertex column
225 // for edges with the same minimum column, sort by their maximum column
226 const layerMaxEdges = []; // same as layerMinEdges, but sorted by maximum column
227 layerMinEdges.forEach(function (arr, lay) {
228 if (!arr)
229 return;
230 arr.sort(function (e1, e2) {
231 const f1c = e1.fromVertex.column;
232 const t1c = e1.toVertex.column;
233 const f2c = e2.fromVertex.column;
234 const t2c = e2.toVertex.column;
235 const e1mincol = Math.min(f1c, t1c);
236 const e2mincol = Math.min(f2c, t2c);
237 if (e1mincol > e2mincol)
238 return 1;
239 if (e1mincol < e2mincol)
240 return -1;
241 const e1maxcol = Math.max(f1c, t1c);
242 const e2maxcol = Math.max(f2c, t2c);
243 if (e1maxcol > e2maxcol)
244 return 1;
245 if (e1maxcol < e2maxcol)
246 return -1;
247 return 0;
248 });
249 layerMaxEdges[lay] = arr.slice(0);
250 layerMaxEdges[lay].sort(function (e1, e2) {
251 const f1c = e1.fromVertex.column;
252 const t1c = e1.toVertex.column;
253 const f2c = e2.fromVertex.column;
254 const t2c = e2.toVertex.column;
255 const e1maxcol = Math.max(f1c, t1c);
256 const e2maxcol = Math.max(f2c, t2c);
257 if (e1maxcol > e2maxcol)
258 return 1;
259 if (e1maxcol < e2maxcol)
260 return -1;
261 const e1mincol = Math.min(f1c, t1c);
262 const e2mincol = Math.min(f2c, t2c);
263 if (e1mincol > e2mincol)
264 return 1;
265 if (e1mincol < e2mincol)
266 return -1;
267 return 0;
268 });
269 });
270 // run through each array of edges to count how many overlaps there might be
271 const layerOverlaps = [];
272 layerMinEdges.forEach(function (arr, lay) {
273 const mins = arr; // sorted by min column
274 const maxs = layerMaxEdges[lay]; // sorted by max column
275 let maxoverlap = 0; // maximum count for this layer
276 if (mins && maxs && mins.length > 1 && maxs.length > 1) {
277 let mini = 0;
278 let min = null;
279 let maxi = 0;
280 let max = null;
281 while (mini < mins.length || maxi < maxs.length) {
282 if (mini < mins.length)
283 min = mins[mini];
284 const mincol = min ? Math.min(min.fromVertex.column, min.toVertex.column) : 0;
285 if (maxi < maxs.length)
286 max = maxs[maxi];
287 const maxcol = max ? Math.max(max.fromVertex.column, max.toVertex.column) : Infinity;
288 maxoverlap = Math.max(maxoverlap, Math.abs(mini - maxi));
289 if (mincol <= maxcol && mini < mins.length) {
290 mini++;
291 }
292 else if (maxi < maxs.length) {
293 maxi++;
294 }
295 }
296 }
297 layerOverlaps[lay] = maxoverlap * 1.5; // # of parallel links
298 });
299 return layerOverlaps;
300 }
301 setupLanes() {
302 // set up some data structures
303 const layout = this;
304 const laneNameSet = new go.Set().addAll(this.laneNames);
305 const laneIndexes = new go.Map(); // lane names --> index when sorted
306 const vit = this.network.vertexes.iterator;
307 while (vit.next()) {
308 const v = vit.value;
309 const lane = this.getLane(v); // cannot call findLane yet
310 if (lane !== null && !laneNameSet.has(lane)) {
311 laneNameSet.add(lane);
312 this.laneNames.push(lane);
313 }
314 const layer = v.layer;
315 if (layer >= 0) {
316 const arr = this._layers[layer];
317 if (!arr) {
318 this._layers[layer] = [v];
319 }
320 else {
321 arr.push(v);
322 }
323 }
324 }
325 // sort laneNames and initialize laneIndexes
326 if (typeof this.laneComparer === "function")
327 this.laneNames.sort(this.laneComparer);
328 for (let i = 0; i < this.laneNames.length; i++) {
329 laneIndexes.add(this.laneNames[i], i);
330 }
331 // now OK to call findLane
332 // sort vertexes so that vertexes are grouped by lane
333 for (let i = 0; i <= this.maxLayer; i++) {
334 this._layers[i].sort(function (a, b) { return layout.compareVertexes(a, b); });
335 }
336 }
337 /**
338 * @hidden
339 * Replace the standard reduceCrossings behavior so that it respects lanes.
340 */
341 reduceCrossings() {
342 this.setupLanes();
343 // this just cares about the .index and ignores .column
344 const layers = this._layers;
345 const red = this.reducer;
346 if (red) {
347 for (let i = 0; i < layers.length - 1; i++) {
348 red.reduceCrossings(layers[i], layers[i + 1]);
349 layers[i].forEach(function (v, j) { v.index = j; });
350 }
351 for (let i = layers.length - 1; i > 0; i--) {
352 red.reduceCrossings(layers[i], layers[i - 1]);
353 layers[i].forEach(function (v, j) { v.index = j; });
354 }
355 }
356 this.computeLanes(); // and recompute all vertex.column values
357 }
358 computeLanes() {
359 // compute needed width for each lane, in columns
360 for (let i = 0; i < this.laneNames.length; i++) {
361 const lane = this.laneNames[i];
362 this.laneBreadths.add(lane, this.computeMinLaneWidth(lane));
363 }
364 const lwidths = new go.Map(); // reused for each layer
365 for (let i = 0; i <= this.maxLayer; i++) {
366 const arr = this._layers[i];
367 if (arr) {
368 const layout = this;
369 // now run through Array finding width (in columns) of each lane
370 // and max with this.laneBreadths.get(lane)
371 for (let j = 0; j < arr.length; j++) {
372 const v = arr[j];
373 const w = this.nodeMinColumnSpace(v, true) + 1 + this.nodeMinColumnSpace(v, false);
374 const ln = this.findLane(v) || "";
375 const totw = lwidths.get(ln);
376 if (totw === null) {
377 lwidths.set(ln, w);
378 }
379 else {
380 lwidths.set(ln, totw + w);
381 }
382 }
383 lwidths.each(function (kvp) {
384 const lane = kvp.key;
385 const colsInLayer = kvp.value;
386 const colsMax = layout.laneBreadths.get(lane) || 0;
387 if (colsInLayer > colsMax)
388 layout.laneBreadths.set(lane, colsInLayer);
389 });
390 lwidths.clear();
391 }
392 }
393 // compute starting positions for each lane
394 let x = 0;
395 for (let i = 0; i < this.laneNames.length; i++) {
396 const lane = this.laneNames[i];
397 this.lanePositions.set(lane, x);
398 const w = this.laneBreadths.get(lane) || 0;
399 x += w + this.laneSpacing;
400 }
401 this.renormalizeColumns();
402 }
403 renormalizeColumns() {
404 // set new column and index on each vertex
405 for (let i = 0; i < this._layers.length; i++) {
406 let prevlane = null;
407 let c = 0;
408 const arr = this._layers[i];
409 for (let j = 0; j < arr.length; j++) {
410 const v = arr[j];
411 v.index = j;
412 const l = this.findLane(v);
413 if (l && prevlane !== l) {
414 c = this.lanePositions.get(l) || 0;
415 const w = this.laneBreadths.get(l) || 0;
416 // compute needed breadth within lane, in columns
417 let z = this.nodeMinColumnSpace(v, true) + 1 + this.nodeMinColumnSpace(v, false);
418 let k = j + 1;
419 while (k < arr.length && this.findLane(arr[k]) === l) {
420 const vz = arr[k];
421 z += this.nodeMinColumnSpace(vz, true) + 1 + this.nodeMinColumnSpace(vz, false);
422 k++;
423 }
424 // if there is extra space, shift the vertexes to the middle of the lane
425 if (z < w) {
426 c += Math.floor((w - z) / 2);
427 }
428 }
429 c += this.nodeMinColumnSpace(v, true);
430 v.column = c;
431 c += 1;
432 c += this.nodeMinColumnSpace(v, false);
433 prevlane = l;
434 }
435 }
436 }
437 /**
438 * Return the minimum lane width, in columns
439 * @param lane
440 */
441 computeMinLaneWidth(lane) { return 0; }
442 /**
443 * @hidden
444 * Disable normal straightenAndPack behavior, which would mess up the columns.
445 */
446 straightenAndPack() { }
447 /**
448 * Given a vertex, get the lane (name) that its node belongs in.
449 * If the lane appears to be undefined, this returns the empty string.
450 * For dummy vertexes (with no node) this will return null.
451 * @param v
452 */
453 getLane(v) {
454 if (v === null)
455 return null;
456 const node = v.node;
457 if (node !== null) {
458 const data = node.data;
459 if (data !== null) {
460 let lane = null;
461 if (typeof this.laneProperty === "function") {
462 lane = this.laneProperty(data);
463 }
464 else {
465 lane = data[this.laneProperty];
466 }
467 if (typeof lane === "string")
468 return lane;
469 return ""; // default lane
470 }
471 }
472 return null;
473 }
474 /**
475 * This is just like {@link #getLane} but handles dummy vertexes
476 * for which the {@link #getLane} returns null by returning the
477 * lane of the edge's source or destination vertex.
478 * This can only be called after the lanes have been set up internally.
479 * @param v
480 */
481 findLane(v) {
482 if (v !== null) {
483 const lane = this.getLane(v);
484 if (lane !== null) {
485 return lane;
486 }
487 else {
488 const srcv = this.findRealSource(v.sourceEdges.first());
489 const dstv = this.findRealDestination(v.destinationEdges.first());
490 const srcLane = this.getLane(srcv);
491 const dstLane = this.getLane(dstv);
492 if (srcLane !== null || dstLane !== null) {
493 if (srcLane === dstLane)
494 return srcLane;
495 if (srcLane !== null)
496 return srcLane;
497 if (dstLane !== null)
498 return dstLane;
499 }
500 }
501 }
502 return null;
503 }
504 findRealSource(e) {
505 if (e === null)
506 return null;
507 const fv = e.fromVertex;
508 if (fv && fv.node)
509 return fv;
510 return this.findRealSource(fv.sourceEdges.first());
511 }
512 findRealDestination(e) {
513 if (e === null)
514 return null;
515 const tv = e.toVertex;
516 if (tv.node)
517 return tv;
518 return this.findRealDestination(tv.destinationEdges.first());
519 }
520 compareVertexes(v, w) {
521 let laneV = this.findLane(v);
522 if (laneV === null)
523 laneV = "";
524 let laneW = this.findLane(w);
525 if (laneW === null)
526 laneW = "";
527 if (laneV < laneW)
528 return -1;
529 if (laneV > laneW)
530 return 1;
531 return 0;
532 }
533 ;
534}