update from RIFT as of 696b75d2fe9fb046261b08c616f1bcf6c0b54a9b third try
[osm/UI.git] / skyquake / plugins / composer / src / src / libraries / graph / layouts / RelationsAndNetworksLayout.js
1 /**
2 * Created by onvelocity on 2/10/16.
3 */
4 import alt from '../../../alt'
5 import d3 from 'd3'
6 import math from '../math'
7 import ClassNames from 'classnames'
8 import ColorGroups from '../../ColorGroups'
9 import GraphVirtualLink from '../GraphVirtualLink'
10 import GraphNetworkService from '../GraphNetworkService'
11 import GraphForwardingGraph from '../GraphForwardingGraph'
12 import GraphConstituentVnfd from '../GraphConstituentVnfd'
13 import GraphVirtualNetworkFunction from '../GraphVirtualNetworkFunction'
14 import SelectionManager from '../../SelectionManager'
15 import GraphConnectionPointNumber from '../GraphConnectionPointNumber'
16 import CatalogItemsActions from '../../../actions/CatalogItemsActions'
17 import DescriptorModelFactory from '../../model/DescriptorModelFactory'
18 import DescriptorGraphSelection from '../DescriptorGraphSelection'
19 import GraphVirtualDeploymentUnit from '../GraphVirtualDeploymentUnit'
20 import GraphRecordServicePath from '../GraphRecordServicePath'
21 import GraphInternalVirtualLink from '../GraphInternalVirtualLink'
22 import TooltipManager from '../../TooltipManager'
23
24 function onCutDelegateToRemove(container) {
25 function onCut(event) {
26 event.target.removeEventListener('cut', onCut);
27 if (container.remove()) {
28 CatalogItemsActions.catalogItemDescriptorChanged.defer(container.getRoot());
29 } else {
30 event.preventDefault();
31 }
32 }
33 this.addEventListener('cut', onCut);
34 }
35
36 export default function RelationsAndNetworksLayout() {
37
38 const graph = this;
39 const props = this.props;
40 const containerWidth = 250;
41 const containerHeight = 55;
42 const marginTop = 20;
43 const marginLeft = 10;
44 const containerList = [];
45 const leftOffset = containerWidth;
46
47 const snapTo = (value) => {
48 return Math.max(props.snapTo * Math.round(value / props.snapTo), props.padding);
49 };
50
51 const line = d3.svg.line()
52 .x(d => {
53 return d.x;
54 })
55 .y(d => {
56 return d.y;
57 });
58
59 function countAncestors(container = {}) {
60 let count = 0;
61 while (container.parent) {
62 count++;
63 container = container.parent;
64 }
65 return count;
66 }
67
68 function renderRelationPath(src, dst) {
69 const path = line.interpolate('basis');
70 const srcPoint = src.position.centerPoint();
71 const dstPoint = dst.position.centerPoint();
72 const angle = math.angleBetweenPositions(src.position, dst.position);
73 if (angle < 180) {
74 srcPoint.y = src.position.top;
75 dstPoint.y = dst.position.bottom;
76 } else {
77 srcPoint.y = src.position.bottom;
78 dstPoint.y = dst.position.top;
79 }
80 return path([srcPoint, dstPoint]);
81 }
82
83 function renderConnectionPath(cpRef) {
84 const path = line.interpolate('basis');
85 const srcPoint = cpRef.position.centerPoint();
86 const dstPoint = cpRef.parent.position.centerPoint();
87 const srcIsTopMounted = /top/.test(cpRef.location);
88 //const srcIsLeftMounted = /left/.test(cpRef.parent.location);
89 const offset = 15;
90 const srcSpline1 = {
91 x: srcPoint.x,
92 y: (srcIsTopMounted ? cpRef.position.top - offset : cpRef.position.bottom + offset)
93 };
94 const srcSpline2 = {
95 x: srcPoint.x,
96 y: (srcIsTopMounted ? cpRef.position.top - offset : cpRef.position.bottom + offset)
97 };
98 return path([srcPoint, srcSpline1, srcSpline2, dstPoint]);
99 }
100
101 const containerLayoutInfo = {
102 nsd: {
103 list: [],
104 width: containerWidth,
105 height: containerHeight,
106 top() {
107 return 10;
108 },
109 left(container, layouts) {
110 const positionIndex = layouts.vnffgd.list.length + this.list.length;
111 return (positionIndex * (this.width * 1.5)) + leftOffset / 2;
112 },
113 renderRelationPath: renderRelationPath,
114 renderConnectionPath: renderConnectionPath
115 },
116 vnffgd: {
117 list: [],
118 width: containerWidth,
119 height: containerHeight,
120 top(container, layouts) {
121 const positionIndex = layouts.nsd.list.length + this.list.length;
122 return (positionIndex * (this.height * 1.5)) + 120;
123 },
124 left(container, layouts) {
125 return 10;
126 },
127 renderRelationPath: renderRelationPath,
128 renderConnectionPath: renderConnectionPath
129 },
130 vnfd: {
131 list: [],
132 width: containerWidth,
133 height: containerHeight,
134 top(container) {
135 return (countAncestors(container) * 100) + 10;
136 },
137 left() {
138 const positionIndex = this.list.length;
139 return (positionIndex * (this.width * 1.5)) + leftOffset;
140 },
141 renderRelationPath: renderRelationPath,
142 renderConnectionPath: renderConnectionPath
143 },
144 'constituent-vnfd': {
145 list: [],
146 width: containerWidth,
147 height: containerHeight,
148 top(container) {
149 return (countAncestors(container) * 100) + 10;
150 },
151 left() {
152 const positionIndex = this.list.length;
153 return (positionIndex * (this.width * 1.5)) + leftOffset;
154 },
155 renderRelationPath: renderRelationPath,
156 renderConnectionPath: renderConnectionPath
157 },
158 pnfd: {
159 list: [],
160 width: containerWidth,
161 height: containerHeight,
162 top(container) {
163 return (countAncestors(container) * 100) + 10;
164 },
165 left() {
166 const positionIndex = this.list.length;
167 return (positionIndex * (this.width * 1.5)) + leftOffset;
168 },
169 renderRelationPath: renderRelationPath,
170 renderConnectionPath: renderConnectionPath
171 },
172 vld: {
173 list: [],
174 width: containerWidth,
175 height: 38,
176 top(container) {
177 return (countAncestors(container) * 100) + 180;
178 },
179 left() {
180 const positionIndex = this.list.length;
181 const marginOffsetFactor = 1.5;
182 const gutterOffsetFactor = 1.5;
183 return (positionIndex * (this.width * gutterOffsetFactor)) + ((this.width * marginOffsetFactor) / 2) + leftOffset;
184 },
185 renderRelationPath: renderRelationPath,
186 renderConnectionPath: renderConnectionPath
187 },
188 'internal-vld': {
189 list: [],
190 width: containerWidth,
191 height: 38,
192 top(container, containerLayouts) {
193 return (countAncestors(container) * 100) + 100;
194 },
195 left() {
196 const positionIndex = this.list.length;
197 const marginOffsetFactor = 1.5;
198 const gutterOffsetFactor = 1.5;
199 return (positionIndex * (this.width * gutterOffsetFactor)) + ((this.width * marginOffsetFactor) / 2) + leftOffset;
200 },
201 renderRelationPath: renderRelationPath,
202 renderConnectionPath: renderConnectionPath
203 },
204 vdu: {
205 list: [],
206 width: containerWidth,
207 height: containerHeight,
208 gutter: 30,
209 top(container) {
210 return (countAncestors(container) * 100) + 10;
211 },
212 left(container) {
213 const positionIndex = this.list.length;
214 return (positionIndex * (this.width * 1.5)) + leftOffset;
215 },
216 renderRelationPath: renderRelationPath,
217 renderConnectionPath: renderConnectionPath
218 }
219 };
220
221 function getConnectionPointEdges() {
222
223 // 1. create a lookup map to find a connection-point by it's key
224 const connectionPointMap = {};
225 containerList.filter(d => d.connectionPoint).reduce((result, container) => {
226 return container.connectionPoint.reduce((result, connectionPoint) => {
227 result[connectionPoint.key] = connectionPoint;
228 connectionPoint.uiState.hasConnection = false;
229 return result;
230 }, result);
231 }, connectionPointMap);
232
233 // 2. determine position of the connection-point and connection-point-ref (they are the same)
234 const connectionPointRefList = [];
235 containerList.filter(container => container.connection).forEach(container => {
236 container.uiState.hasConnection = false;
237 container.connection.filter(d => d.key).forEach(cpRef => {
238 try {
239 const source = connectionPointMap[cpRef.key];
240 const destination = container;
241 source.uiState.hasConnection = true;
242 destination.uiState.hasConnection = true;
243 const edgeStateMachine = math.upperLowerEdgeLocation;
244 // angle is used to determine location top, bottom, right, left
245 const angle = math.angleBetweenPositions(source.parent.position, destination.position);
246 // distance is used to determine order of the connection points
247 const distance = math.distanceBetweenPositions(source.parent.position, destination.position);
248 cpRef.location = source.location = edgeStateMachine(angle);
249 source.edgeAngle = angle;
250 if (destination.type === 'vdu') {
251 source.edgeLength = Math.max(source.edgeLength || 0, distance);
252 }
253 // warn assigning same instance (e.g. pass by reference) so that changes will reflect thru
254 cpRef.position = source.position;
255 connectionPointRefList.push(cpRef);
256 } catch (e) {
257 return;
258 }
259 });
260 });
261
262 // 3. update the connection-point/-ref location based on the angle of the path
263 containerList.filter(d => d.connectionPoint).forEach(container => {
264 // group the connectors by their location and then update their position coordinates accordingly
265 const connectionPoints = container.connectionPoint.sort((a, b) => b.edgeLength - a.edgeLength);
266 const locationIndexCounters = {};
267 connectionPoints.forEach(connectionPoint => {
268 // location index is used to calculate the actual position where the path will terminate on the container
269 const location = connectionPoint.location;
270 const locationIndex = locationIndexCounters[location] || (locationIndexCounters[location] = 0);
271 connectionPoint.positionIndex = locationIndex;
272 if (/left/.test(location)) {
273 connectionPoint.position.moveLeft(connectionPoint.parent.position.left + 5 + ((connectionPoint.width + 1) * locationIndex));
274 } else {
275 connectionPoint.position.moveRight(connectionPoint.parent.position.right - 15 - ((connectionPoint.width + 1) * locationIndex));
276 }
277 if (/top/.test(location)) {
278 connectionPoint.position.moveTop(connectionPoint.parent.position.top - connectionPoint.height);
279 } else {
280 connectionPoint.position.moveTop(connectionPoint.parent.position.bottom);
281 }
282 locationIndexCounters[location] = locationIndex + 1;
283 });
284 });
285
286 return connectionPointRefList;
287
288 }
289
290 function drawConnectionPointsAndPaths(graph, connectionPointRefs) {
291
292 const paths = graph.paths.selectAll('.connection').data(connectionPointRefs, DescriptorModelFactory.containerIdentity);
293
294 paths.enter().append('path');
295
296 paths.attr({
297 'data-uid': d => {
298 return d.uid;
299 },
300 'class': d => {
301 return 'connection between-' + d.parent.type + '-and-' + d.type;
302 },
303 'stroke-width': 5,
304 stroke: ColorGroups.vld.primary,
305 fill: 'transparent',
306 d: edge => {
307 const layout = containerLayoutInfo[edge.parent.type];
308 return layout.renderConnectionPath(edge, containerLayoutInfo);
309 }
310 }).on('cut', (container) => {
311
312 let success = false;
313
314 if (container && container.remove) {
315 success = container.remove();
316 }
317
318 if (success) {
319 CatalogItemsActions.catalogItemDescriptorChanged.defer(container.getRoot());
320 } else {
321 d3.event.preventDefault();
322 }
323
324 d3.event.stopPropagation();
325
326 });
327
328 paths.exit().remove();
329
330 const symbolSize = props.connectionPointSize;
331 const connectionPointSymbol = d3.svg.symbol().type('square').size(symbolSize);
332 const internalConnectionPointSymbolBottom = d3.svg.symbol().type('triangle-down').size(symbolSize);
333 const internalConnectionPointSymbolTop = d3.svg.symbol().type('triangle-up').size(symbolSize);
334
335 const connectors = containerList.filter(d => d.connectors).reduce((result, container) => {
336 return container.connectors.reduce((result, connector) => {
337 result.add(connector);
338 return result;
339 }, result);
340 }, new Set());
341
342 const points = graph.connectorsGroup.selectAll('.connector').data(Array.from(connectors), DescriptorModelFactory.containerIdentity);
343
344 points.enter().append('path');
345
346 points.attr({
347 'data-uid': d => d.uid,
348 'data-key': d => d.key,
349 'data-cp-number': d => d.uiState.cpNumber,
350 'class': d => {
351 return ClassNames('connector', d.type, d.parent.type, {
352 '-is-connected': d.uiState.hasConnection,
353 '-is-not-connected': !d.uiState.hasConnection
354 });
355 },
356 'data-tip': d => {
357 const info = d.displayData;
358 return Object.keys(info).reduce((r, key) => {
359 if (info[key]) {
360 return r + `<div class="${key}"><i>${key}</i><val>${info[key]}</val></div>`;
361 }
362 return r;
363 }, '');
364 },
365 'data-tip-offset': d => {
366 if (d.type === 'internal-connection-point') {
367 return '{"top": -7, "left": -9}';
368 }
369 return '{"top": -5, "left": -5}';
370 },
371 transform: d => {
372 const point = d.position.centerPoint();
373 return 'translate(' + (point.x) + ', ' + (point.y) + ')';
374 },
375 d: d => {
376 if (d.type === 'connection-point') {
377 return connectionPointSymbol();
378 }
379 if (/top/.test(d.location)) {
380 return internalConnectionPointSymbolTop();
381 }
382 return internalConnectionPointSymbolBottom();
383 }
384 }).on('cut', (container) => {
385
386 let success = false;
387
388 if (container && container.remove) {
389 success = container.remove();
390 }
391
392 if (success) {
393 CatalogItemsActions.catalogItemDescriptorChanged.defer(container.getRoot());
394 } else {
395 d3.event.preventDefault();
396 }
397
398 d3.event.stopPropagation();
399
400 }).on('mouseenter', () => {
401 TooltipManager.showTooltip(d3.event.target);
402 });
403
404 points.exit().remove();
405
406 const test = new GraphConnectionPointNumber(graph);
407 test.addContainers(Array.from(connectors));
408 test.render();
409 }
410
411 function drawRelationPointsAndPaths(graph, relationEdges) {
412
413 const paths = graph.paths.selectAll('.relation').data(relationEdges, DescriptorModelFactory.containerIdentity);
414
415 paths.enter().append('path')
416 .attr({
417 'class': d => {
418 return ClassNames('relation', d.type, {
419 '-is-selected': d.uiState && SelectionManager.isSelected(d) /*d.uiState && d.uiState.selected*/
420 });
421 },
422 stroke: 'red',
423 fill: 'transparent',
424 'marker-start': 'url(#relation-marker-end)',
425 'marker-end': 'url(#relation-marker-end)'
426 });
427
428 paths.attr({
429 d: d => {
430 const src = d;
431 const dst = d.parent;
432 const layout = containerLayoutInfo[src.type];
433 return layout.renderRelationPath(src, dst, containerLayoutInfo);
434 }
435 });
436
437 paths.exit().remove();
438
439 }
440
441 function updateContainerPosition(graph, container, layout) {
442 // use the provided layout to generate the position coordinates
443 const position = container.position;
444 position.top = layout.top(container, containerLayoutInfo) + marginTop;
445 position.left = layout.left(container, containerLayoutInfo) + marginLeft;
446 position.width = layout.width;
447 position.height = layout.height;
448 // cache the default layout position which may be needed by the layout
449 // of children elements that have not been positioned by the user
450 container.uiState.defaultLayoutPosition = position.value();
451 const savedContainerPosition = graph.lookupSavedContainerPosition(container);
452 if (savedContainerPosition) {
453 // set the container position with the saved position coordinates
454 container.setPosition(savedContainerPosition);
455 }
456 if (container.uiState.dropCoordinates) {
457 const rect = graph.svg.node().getBoundingClientRect();
458 const top = container.uiState.dropCoordinates.clientY - (position.height / 2) - rect.top;
459 const left = container.uiState.dropCoordinates.clientX - (position.width / 2) - rect.left;
460 container.position.move(Math.max(props.padding, left), Math.max(props.padding, top));
461 graph.saveContainerPosition(container);
462 delete container.uiState.dropCoordinates;
463 } else {
464 graph.saveContainerPosition(container);
465 }
466 }
467
468 return {
469
470 addContainers(containers) {
471
472 const layout = this;
473
474 //containers = containers.filter(d => containerLayouts[d.type]);
475
476 const graphSize = {
477 width: 0,
478 height: 0
479 };
480
481 containers.forEach(container => {
482 containerList.push(container);
483 });
484
485 containers.forEach(container => {
486 const layout = containerLayoutInfo[container.type];
487 if (!layout) {
488 return
489 //throw new ReferenceError('unknown container type: ' + container.type);
490 }
491 updateContainerPosition(graph, container, layout);
492 layout.list.push(container);
493 graphSize.width = Math.max(graphSize.width, container.position.right, props.width);
494 graphSize.height = Math.max(graphSize.height, container.position.bottom, props.height);
495 });
496
497 graph.svg.attr({
498 width: graphSize.width + props.width,
499 height: graphSize.height + props.height
500 });
501
502 const uiTransientState = {
503 isDragging: false,
504 dragStartInfo: [0, 0]
505 };
506
507 // todo extract drag behavior into class DescriptorGraphDrag
508
509 const drag = this.drag = d3.behavior.drag()
510 .origin(function (d) {
511 return d;
512 })
513 .on('drag.graph', function (d) {
514 uiTransientState.isDragging = true;
515 const mouse = d3.mouse(graph.g.node());
516 const offset = uiTransientState.dragStartInfo;
517 const newTopEdge = snapTo(mouse[1] - offset[1]);
518 const newLeftEdge = snapTo(mouse[0] - offset[0]);
519 if (d.position.left === newLeftEdge && d.position.top === newTopEdge) {
520 // do not redraw since we are not moving the container
521 return;
522 }
523 d.position.move(newLeftEdge, newTopEdge);
524 graph.saveContainerPosition(d);
525 const connectionPointRefs = getConnectionPointEdges();
526 d3.select(this).attr({
527 transform: () => {
528 const x = d.position.left;
529 const y = d.position.top;
530 return 'translate(' + x + ', ' + y + ')';
531 }
532 });
533 requestAnimationFrame(() => {
534 drawConnectionPointsAndPaths(graph, connectionPointRefs);
535 layout.renderers.forEach(d => d.render());
536 });
537 }).on('dragend.graph', () => {
538 // d3 fires a drag-end event on mouse up, even if just clicking
539 if (uiTransientState.isDragging) {
540 uiTransientState.isDragging = false;
541 CatalogItemsActions.catalogItemMetaDataChanged(graph.containers[0].model);
542 d3.select(this).on('.graph', null);
543 }
544 }).on('dragstart.graph', function (d) {
545 // the x, y offset of the mouse from the container's left, top
546 uiTransientState.dragStartInfo = d3.mouse(this);
547 });
548
549 this.renderers = [GraphVirtualLink, GraphNetworkService, GraphForwardingGraph, GraphVirtualNetworkFunction, GraphConstituentVnfd, GraphVirtualDeploymentUnit, GraphRecordServicePath, GraphInternalVirtualLink].map(layout => {
550 const container = new layout(graph, props);
551 const layoutInfo = containerLayoutInfo[container.classType && container.classType.type];
552 if (layoutInfo) {
553 container.props.descriptorWidth = layoutInfo.width;
554 container.props.descriptorHeight = layoutInfo.height;
555 }
556 if (!props.readOnly) {
557 container.dragHandler = drag;
558 }
559 container.addContainers(containerList);
560 return container;
561 });
562
563 },
564
565 render(graph, updateCallback = () => {}) {
566 const connectionPointRefs = getConnectionPointEdges();
567 requestAnimationFrame(() => {
568 drawConnectionPointsAndPaths(graph, connectionPointRefs);
569 this.renderers.forEach(d => d.render());
570 updateCallback();
571 });
572 }
573
574 };
575
576 }