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