2 * Created by onvelocity on 2/10/16.
4 import alt from '../../../alt'
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'
25 function onCutDelegateToRemove(container) {
26 function onCut(event) {
27 event.target.removeEventListener('cut', onCut);
28 if (container.remove()) {
29 CatalogItemsActions.catalogItemDescriptorChanged.defer(container.getRoot());
31 event.preventDefault();
34 this.addEventListener('cut', onCut);
37 export default function RelationsAndNetworksLayout() {
40 const props = this.props;
41 const containerWidth = 250;
42 const containerHeight = 55;
44 const marginLeft = 10;
45 const containerList = [];
46 const leftOffset = containerWidth;
48 const snapTo = (value) => {
49 return Math.max(props.snapTo * Math.round(value / props.snapTo), props.padding);
52 const line = d3.svg.line()
60 function countAncestors(container = {}) {
62 while (container.parent) {
64 container = container.parent;
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);
75 srcPoint.y = src.position.top;
76 dstPoint.y = dst.position.bottom;
78 srcPoint.y = src.position.bottom;
79 dstPoint.y = dst.position.top;
81 return path([srcPoint, dstPoint]);
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);
93 y: (srcIsTopMounted ? cpRef.position.top - offset : cpRef.position.bottom + offset)
97 y: (srcIsTopMounted ? cpRef.position.top - offset : cpRef.position.bottom + offset)
99 return path([srcPoint, srcSpline1, srcSpline2, dstPoint]);
102 const containerLayoutInfo = {
105 width: containerWidth,
106 height: containerHeight,
110 left(container, layouts) {
111 const positionIndex = layouts.vnffgd.list.length + this.list.length;
112 return (positionIndex * (this.width * 1.5)) + leftOffset / 2;
114 renderRelationPath: renderRelationPath,
115 renderConnectionPath: renderConnectionPath
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;
125 left(container, layouts) {
128 renderRelationPath: renderRelationPath,
129 renderConnectionPath: renderConnectionPath
133 width: containerWidth,
134 height: containerHeight,
136 return (countAncestors(container) * 100) + 10;
139 const positionIndex = this.list.length;
140 return (positionIndex * (this.width * 1.5)) + leftOffset;
142 renderRelationPath: renderRelationPath,
143 renderConnectionPath: renderConnectionPath
145 'constituent-vnfd': {
147 width: containerWidth,
148 height: containerHeight,
150 return (countAncestors(container) * 100) + 10;
153 const positionIndex = this.list.length;
154 return (positionIndex * (this.width * 1.5)) + leftOffset;
156 renderRelationPath: renderRelationPath,
157 renderConnectionPath: renderConnectionPath
161 width: containerWidth,
162 height: containerHeight,
164 return (countAncestors(container) * 100) + 10;
167 const positionIndex = this.list.length;
168 return (positionIndex * (this.width * 1.5)) + leftOffset;
170 renderRelationPath: renderRelationPath,
171 renderConnectionPath: renderConnectionPath
175 width: containerWidth,
178 return (countAncestors(container) * 100) + 180;
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;
186 renderRelationPath: renderRelationPath,
187 renderConnectionPath: renderConnectionPath
191 width: containerWidth,
193 top(container, containerLayouts) {
194 return (countAncestors(container) * 100) + 100;
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;
202 renderRelationPath: renderRelationPath,
203 renderConnectionPath: renderConnectionPath
207 width: containerWidth,
208 height: containerHeight,
211 return (countAncestors(container) * 100) + 10;
214 const positionIndex = this.list.length;
215 return (positionIndex * (this.width * 1.5)) + leftOffset;
217 renderRelationPath: renderRelationPath,
218 renderConnectionPath: renderConnectionPath
222 function getConnectionPointEdges() {
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;
232 }, connectionPointMap);
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);
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);
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));
272 connectionPoint.position.moveRight(connectionPoint.parent.position.right - 15 - ((connectionPoint.width + 1) * locationIndex));
274 if (/top/.test(location)) {
275 connectionPoint.position.moveTop(connectionPoint.parent.position.top - connectionPoint.height);
277 connectionPoint.position.moveTop(connectionPoint.parent.position.bottom);
279 locationIndexCounters[location] = locationIndex + 1;
283 return connectionPointRefList;
287 function drawConnectionPointsAndPaths(graph, connectionPointRefs) {
289 const paths = graph.paths.selectAll('.connection').data(connectionPointRefs, DescriptorModelFactory.containerIdentity);
291 paths.enter().append('path');
298 return 'connection between-' + d.parent.type + '-and-' + d.type;
301 stroke: ColorGroups.vld.primary,
304 const layout = containerLayoutInfo[edge.parent.type];
305 return layout.renderConnectionPath(edge, containerLayoutInfo);
307 }).on('cut', (container) => {
311 if (container && container.remove) {
312 success = container.remove();
316 CatalogItemsActions.catalogItemDescriptorChanged.defer(container.getRoot());
318 d3.event.preventDefault();
321 d3.event.stopPropagation();
325 paths.exit().remove();
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);
332 const connectors = containerList.filter(d => d.connectors).reduce((result, container) => {
333 return container.connectors.reduce((result, connector) => {
334 result.add(connector);
339 const points = graph.connectorsGroup.selectAll('.connector').data(Array.from(connectors), DescriptorModelFactory.containerIdentity);
341 points.enter().append('path');
344 'data-uid': d => d.uid,
345 'data-key': d => d.key,
346 'data-cp-number': d => d.uiState.cpNumber,
348 return ClassNames('connector', d.type, d.parent.type, {
349 '-is-connected': d.uiState.hasConnection,
350 '-is-not-connected': !d.uiState.hasConnection
354 const info = d.displayData;
355 return Object.keys(info).reduce((r, key) => {
357 return r + `<div class="${key}"><i>${key}</i><val>${info[key]}</val></div>`;
362 'data-tip-offset': d => {
363 if (d.type === 'internal-connection-point') {
364 return '{"top": -7, "left": -9}';
366 return '{"top": -5, "left": -5}';
369 const point = d.position.centerPoint();
370 return 'translate(' + (point.x) + ', ' + (point.y) + ')';
373 if (d.type === 'connection-point') {
374 return connectionPointSymbol();
376 if (/top/.test(d.location)) {
377 return internalConnectionPointSymbolTop();
379 return internalConnectionPointSymbolBottom();
381 }).on('cut', (container) => {
385 if (container && container.remove) {
386 success = container.remove();
390 CatalogItemsActions.catalogItemDescriptorChanged.defer(container.getRoot());
392 d3.event.preventDefault();
395 d3.event.stopPropagation();
397 }).on('mouseenter', () => {
398 TooltipManager.showTooltip(d3.event.target);
401 points.exit().remove();
403 const test = new GraphConnectionPointNumber(graph);
404 test.addContainers(Array.from(connectors));
408 function drawRelationPointsAndPaths (graph, relationEdges) {
410 const paths = graph.paths.selectAll('.relation').data(relationEdges, DescriptorModelFactory.containerIdentity);
412 paths.enter().append('path')
415 return ClassNames('relation', d.type, {'-is-selected': d.uiState && SelectionManager.isSelected(d) /*d.uiState && d.uiState.selected*/});
419 'marker-start': 'url(#relation-marker-end)',
420 'marker-end': 'url(#relation-marker-end)'
426 const dst = d.parent;
427 const layout = containerLayoutInfo[src.type];
428 return layout.renderRelationPath(src, dst, containerLayoutInfo);
432 paths.exit().remove();
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);
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;
459 graph.saveContainerPosition(container);
465 addContainers(containers) {
469 //containers = containers.filter(d => containerLayouts[d.type]);
476 containers.forEach(container => {
477 containerList.push(container);
480 containers.forEach(container => {
481 const layout = containerLayoutInfo[container.type];
484 //throw new ReferenceError('unknown container type: ' + container.type);
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);
493 width: graphSize.width + props.width,
494 height: graphSize.height + props.height
497 const uiTransientState = {
499 dragStartInfo: [0, 0]
502 // todo extract drag behavior into class DescriptorGraphDrag
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
516 d.position.move(newLeftEdge, newTopEdge);
517 graph.saveContainerPosition(d);
518 const connectionPointRefs = getConnectionPointEdges();
519 d3.select(this).attr({
521 const x = d.position.left;
522 const y = d.position.top;
523 return 'translate(' + x + ', ' + y + ')';
526 requestAnimationFrame(() => {
527 drawConnectionPointsAndPaths(graph, connectionPointRefs);
528 layout.renderers.forEach(d => d.render());
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);
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);
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];
546 container.props.descriptorWidth = layoutInfo.width;
547 container.props.descriptorHeight = layoutInfo.height;
549 container.dragHandler = drag;
550 container.addContainers(containerList);
556 render(graph, updateCallback = () => {}) {
557 const connectionPointRefs = getConnectionPointEdges();
558 requestAnimationFrame(() => {
559 drawConnectionPointsAndPaths(graph, connectionPointRefs);
560 this.renderers.forEach(d => d.render());