Rift.IO OSM R1 Initial Submission
[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 _ 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 try {
240 const source = connectionPointMap[cpRef.key];
241 const destination = container;
242 source.uiState.hasConnection = true;
243 destination.uiState.hasConnection = true;
244 const edgeStateMachine = math.upperLowerEdgeLocation;
245 // angle is used to determine location top, bottom, right, left
246 const angle = math.angleBetweenPositions(source.parent.position, destination.position);
247 // distance is used to determine order of the connection points
248 const distance = math.distanceBetweenPositions(source.parent.position, destination.position);
249 cpRef.location = source.location = edgeStateMachine(angle);
250 source.edgeAngle = angle;
251 if (destination.type === 'vdu') {
252 source.edgeLength = Math.max(source.edgeLength || 0, distance);
253 }
254 // warn assigning same instance (e.g. pass by reference) so that changes will reflect thru
255 cpRef.position = source.position;
256 connectionPointRefList.push(cpRef);
257 } catch(e) {
258 return;
259 }
260 });
261 });
262
263 // 3. update the connection-point/-ref location based on the angle of the path
264 containerList.filter(d => d.connectionPoint).forEach(container => {
265 // group the connectors by their location and then update their position coordinates accordingly
266 const connectionPoints = container.connectionPoint.sort((a, b) => b.edgeLength - a.edgeLength);
267 const locationIndexCounters = {};
268 connectionPoints.forEach(connectionPoint => {
269 // location index is used to calculate the actual position where the path will terminate on the container
270 const location = connectionPoint.location;
271 const locationIndex = locationIndexCounters[location] || (locationIndexCounters[location] = 0);
272 connectionPoint.positionIndex = locationIndex;
273 if (/left/.test(location)) {
274 connectionPoint.position.moveLeft(connectionPoint.parent.position.left + 5 + ((connectionPoint.width + 1) * locationIndex));
275 } else {
276 connectionPoint.position.moveRight(connectionPoint.parent.position.right - 15 - ((connectionPoint.width + 1) * locationIndex));
277 }
278 if (/top/.test(location)) {
279 connectionPoint.position.moveTop(connectionPoint.parent.position.top - connectionPoint.height);
280 } else {
281 connectionPoint.position.moveTop(connectionPoint.parent.position.bottom);
282 }
283 locationIndexCounters[location] = locationIndex + 1;
284 });
285 });
286
287 return connectionPointRefList;
288
289 }
290
291 function drawConnectionPointsAndPaths(graph, connectionPointRefs) {
292
293 const paths = graph.paths.selectAll('.connection').data(connectionPointRefs, DescriptorModelFactory.containerIdentity);
294
295 paths.enter().append('path');
296
297 paths.attr({
298 'data-uid': d => {
299 return d.uid;
300 },
301 'class': d => {
302 return 'connection between-' + d.parent.type + '-and-' + d.type;
303 },
304 'stroke-width': 5,
305 stroke: ColorGroups.vld.primary,
306 fill: 'transparent',
307 d: edge => {
308 const layout = containerLayoutInfo[edge.parent.type];
309 return layout.renderConnectionPath(edge, containerLayoutInfo);
310 }
311 }).on('cut', (container) => {
312
313 let success = false;
314
315 if (container && container.remove) {
316 success = container.remove();
317 }
318
319 if (success) {
320 CatalogItemsActions.catalogItemDescriptorChanged.defer(container.getRoot());
321 } else {
322 d3.event.preventDefault();
323 }
324
325 d3.event.stopPropagation();
326
327 });
328
329 paths.exit().remove();
330
331 const symbolSize = props.connectionPointSize;
332 const connectionPointSymbol = d3.svg.symbol().type('square').size(symbolSize);
333 const internalConnectionPointSymbolBottom = d3.svg.symbol().type('triangle-down').size(symbolSize);
334 const internalConnectionPointSymbolTop = d3.svg.symbol().type('triangle-up').size(symbolSize);
335
336 const connectors = containerList.filter(d => d.connectors).reduce((result, container) => {
337 return container.connectors.reduce((result, connector) => {
338 result.add(connector);
339 return result;
340 }, result);
341 }, new Set());
342
343 const points = graph.connectorsGroup.selectAll('.connector').data(Array.from(connectors), DescriptorModelFactory.containerIdentity);
344
345 points.enter().append('path');
346
347 points.attr({
348 'data-uid': d => d.uid,
349 'data-key': d => d.key,
350 'data-cp-number': d => d.uiState.cpNumber,
351 'class': d => {
352 return ClassNames('connector', d.type, d.parent.type, {
353 '-is-connected': d.uiState.hasConnection,
354 '-is-not-connected': !d.uiState.hasConnection
355 });
356 },
357 'data-tip': d => {
358 const info = d.displayData;
359 return Object.keys(info).reduce((r, key) => {
360 if (info[key]) {
361 return r + `<div class="${key}"><i>${key}</i><val>${info[key]}</val></div>`;
362 }
363 return r;
364 }, '');
365 },
366 'data-tip-offset': d => {
367 if (d.type === 'internal-connection-point') {
368 return '{"top": -7, "left": -9}';
369 }
370 return '{"top": -5, "left": -5}';
371 },
372 transform: d => {
373 const point = d.position.centerPoint();
374 return 'translate(' + (point.x) + ', ' + (point.y) + ')';
375 },
376 d: d => {
377 if (d.type === 'connection-point') {
378 return connectionPointSymbol();
379 }
380 if (/top/.test(d.location)) {
381 return internalConnectionPointSymbolTop();
382 }
383 return internalConnectionPointSymbolBottom();
384 }
385 }).on('cut', (container) => {
386
387 let success = false;
388
389 if (container && container.remove) {
390 success = container.remove();
391 }
392
393 if (success) {
394 CatalogItemsActions.catalogItemDescriptorChanged.defer(container.getRoot());
395 } else {
396 d3.event.preventDefault();
397 }
398
399 d3.event.stopPropagation();
400
401 }).on('mouseenter', () => {
402 TooltipManager.showTooltip(d3.event.target);
403 });
404
405 points.exit().remove();
406
407 const test = new GraphConnectionPointNumber(graph);
408 test.addContainers(Array.from(connectors));
409 test.render();
410 }
411
412 function drawRelationPointsAndPaths (graph, relationEdges) {
413
414 const paths = graph.paths.selectAll('.relation').data(relationEdges, DescriptorModelFactory.containerIdentity);
415
416 paths.enter().append('path')
417 .attr({
418 'class': d => {
419 return ClassNames('relation', d.type, {'-is-selected': d.uiState && SelectionManager.isSelected(d) /*d.uiState && d.uiState.selected*/});
420 },
421 stroke: 'red',
422 fill: 'transparent',
423 'marker-start': 'url(#relation-marker-end)',
424 'marker-end': 'url(#relation-marker-end)'
425 });
426
427 paths.attr({
428 d: d => {
429 const src = d;
430 const dst = d.parent;
431 const layout = containerLayoutInfo[src.type];
432 return layout.renderRelationPath(src, dst, containerLayoutInfo);
433 }
434 });
435
436 paths.exit().remove();
437
438 }
439
440 function updateContainerPosition(graph, container, layout) {
441 // use the provided layout to generate the position coordinates
442 const position = container.position;
443 position.top = layout.top(container, containerLayoutInfo) + marginTop;
444 position.left = layout.left(container, containerLayoutInfo) + marginLeft;
445 position.width = layout.width;
446 position.height = layout.height;
447 // cache the default layout position which may be needed by the layout
448 // of children elements that have not been positioned by the user
449 container.uiState.defaultLayoutPosition = position.value();
450 const savedContainerPosition = graph.lookupSavedContainerPosition(container);
451 if (savedContainerPosition) {
452 // set the container position with the saved position coordinates
453 container.setPosition(savedContainerPosition);
454 }
455 if (container.uiState.dropCoordinates) {
456 const rect = graph.svg.node().getBoundingClientRect();
457 const top = container.uiState.dropCoordinates.clientY - (position.height / 2) - rect.top;
458 const left = container.uiState.dropCoordinates.clientX - (position.width / 2) - rect.left;
459 container.position.move(Math.max(props.padding, left), Math.max(props.padding, top));
460 graph.saveContainerPosition(container);
461 delete container.uiState.dropCoordinates;
462 } else {
463 graph.saveContainerPosition(container);
464 }
465 }
466
467 return {
468
469 addContainers(containers) {
470
471 const layout = this;
472
473 //containers = containers.filter(d => containerLayouts[d.type]);
474
475 const graphSize = {
476 width: 0,
477 height: 0
478 };
479
480 containers.forEach(container => {
481 containerList.push(container);
482 });
483
484 containers.forEach(container => {
485 const layout = containerLayoutInfo[container.type];
486 if (!layout) {
487 return
488 //throw new ReferenceError('unknown container type: ' + container.type);
489 }
490 updateContainerPosition(graph, container, layout);
491 layout.list.push(container);
492 graphSize.width = Math.max(graphSize.width, container.position.right, props.width);
493 graphSize.height = Math.max(graphSize.height, container.position.bottom, props.height);
494 });
495
496 graph.svg.attr({
497 width: graphSize.width + props.width,
498 height: graphSize.height + props.height
499 });
500
501 const uiTransientState = {
502 isDragging: false,
503 dragStartInfo: [0, 0]
504 };
505
506 // todo extract drag behavior into class DescriptorGraphDrag
507
508 const drag = this.drag = d3.behavior.drag()
509 .origin(function(d) { return d; })
510 .on('drag.graph', function (d) {
511 uiTransientState.isDragging = true;
512 const mouse = d3.mouse(graph.g.node());
513 const offset = uiTransientState.dragStartInfo;
514 const newTopEdge = snapTo(mouse[1] - offset[1]);
515 const newLeftEdge = snapTo(mouse[0] - offset[0]);
516 if (d.position.left === newLeftEdge && d.position.top === newTopEdge) {
517 // do not redraw since we are not moving the container
518 return;
519 }
520 d.position.move(newLeftEdge, newTopEdge);
521 graph.saveContainerPosition(d);
522 const connectionPointRefs = getConnectionPointEdges();
523 d3.select(this).attr({
524 transform: () => {
525 const x = d.position.left;
526 const y = d.position.top;
527 return 'translate(' + x + ', ' + y + ')';
528 }
529 });
530 requestAnimationFrame(() => {
531 drawConnectionPointsAndPaths(graph, connectionPointRefs);
532 layout.renderers.forEach(d => d.render());
533 });
534 }).on('dragend.graph', () => {
535 // d3 fires a drag-end event on mouse up, even if just clicking
536 if (uiTransientState.isDragging) {
537 uiTransientState.isDragging = false;
538 CatalogItemsActions.catalogItemMetaDataChanged(graph.containers[0].model);
539 d3.select(this).on('.graph', null);
540 }
541 }).on('dragstart.graph', function (d) {
542 // the x, y offset of the mouse from the container's left, top
543 uiTransientState.dragStartInfo = d3.mouse(this);
544 });
545
546 this.renderers = [GraphVirtualLink, GraphNetworkService, GraphForwardingGraph, GraphVirtualNetworkFunction, GraphConstituentVnfd, GraphVirtualDeploymentUnit, GraphRecordServicePath, GraphInternalVirtualLink].map(layout => {
547 const container = new layout(graph, props);
548 const layoutInfo = containerLayoutInfo[container.classType && container.classType.type];
549 if (layoutInfo) {
550 container.props.descriptorWidth = layoutInfo.width;
551 container.props.descriptorHeight = layoutInfo.height;
552 }
553 container.dragHandler = drag;
554 container.addContainers(containerList);
555 return container;
556 });
557
558 },
559
560 render(graph, updateCallback = () => {}) {
561 const connectionPointRefs = getConnectionPointEdges();
562 requestAnimationFrame(() => {
563 drawConnectionPointsAndPaths(graph, connectionPointRefs);
564 this.renderers.forEach(d => d.render());
565 updateCallback();
566 });
567 }
568
569 };
570
571 }