--- /dev/null
+/**
+ * Created by onvelocity on 2/10/16.
+ */
+import alt from '../../../alt'
+import _ from 'lodash'
+import d3 from 'd3'
+import math from '../math'
+import ClassNames from 'classnames'
+import ColorGroups from '../../ColorGroups'
+import GraphVirtualLink from '../GraphVirtualLink'
+import GraphNetworkService from '../GraphNetworkService'
+import GraphForwardingGraph from '../GraphForwardingGraph'
+import GraphConstituentVnfd from '../GraphConstituentVnfd'
+import GraphVirtualNetworkFunction from '../GraphVirtualNetworkFunction'
+import SelectionManager from '../../SelectionManager'
+import GraphConnectionPointNumber from '../GraphConnectionPointNumber'
+import CatalogItemsActions from '../../../actions/CatalogItemsActions'
+import DescriptorModelFactory from '../../model/DescriptorModelFactory'
+import DescriptorGraphSelection from '../DescriptorGraphSelection'
+import GraphVirtualDeploymentUnit from '../GraphVirtualDeploymentUnit'
+import GraphRecordServicePath from '../GraphRecordServicePath'
+import GraphInternalVirtualLink from '../GraphInternalVirtualLink'
+import TooltipManager from '../../TooltipManager'
+
+function onCutDelegateToRemove(container) {
+ function onCut(event) {
+ event.target.removeEventListener('cut', onCut);
+ if (container.remove()) {
+ CatalogItemsActions.catalogItemDescriptorChanged.defer(container.getRoot());
+ } else {
+ event.preventDefault();
+ }
+ }
+ this.addEventListener('cut', onCut);
+}
+
+export default function RelationsAndNetworksLayout() {
+
+ const graph = this;
+ const props = this.props;
+ const containerWidth = 250;
+ const containerHeight = 55;
+ const marginTop = 20;
+ const marginLeft = 10;
+ const containerList = [];
+ const leftOffset = containerWidth;
+
+ const snapTo = (value) => {
+ return Math.max(props.snapTo * Math.round(value / props.snapTo), props.padding);
+ };
+
+ const line = d3.svg.line()
+ .x(d => {
+ return d.x;
+ })
+ .y(d => {
+ return d.y;
+ });
+
+ function countAncestors(container = {}) {
+ let count = 0;
+ while (container.parent) {
+ count++;
+ container = container.parent;
+ }
+ return count;
+ }
+
+ function renderRelationPath(src, dst) {
+ const path = line.interpolate('basis');
+ const srcPoint = src.position.centerPoint();
+ const dstPoint = dst.position.centerPoint();
+ const angle = math.angleBetweenPositions(src.position, dst.position);
+ if (angle < 180) {
+ srcPoint.y = src.position.top;
+ dstPoint.y = dst.position.bottom;
+ } else {
+ srcPoint.y = src.position.bottom;
+ dstPoint.y = dst.position.top;
+ }
+ return path([srcPoint, dstPoint]);
+ }
+
+ function renderConnectionPath(cpRef) {
+ const path = line.interpolate('basis');
+ const srcPoint = cpRef.position.centerPoint();
+ const dstPoint = cpRef.parent.position.centerPoint();
+ const srcIsTopMounted = /top/.test(cpRef.location);
+ //const srcIsLeftMounted = /left/.test(cpRef.parent.location);
+ const offset = 15;
+ const srcSpline1 = {
+ x: srcPoint.x,
+ y: (srcIsTopMounted ? cpRef.position.top - offset : cpRef.position.bottom + offset)
+ };
+ const srcSpline2 = {
+ x: srcPoint.x,
+ y: (srcIsTopMounted ? cpRef.position.top - offset : cpRef.position.bottom + offset)
+ };
+ return path([srcPoint, srcSpline1, srcSpline2, dstPoint]);
+ }
+
+ const containerLayoutInfo = {
+ nsd: {
+ list: [],
+ width: containerWidth,
+ height: containerHeight,
+ top() {
+ return 10;
+ },
+ left(container, layouts) {
+ const positionIndex = layouts.vnffgd.list.length + this.list.length;
+ return (positionIndex * (this.width * 1.5)) + leftOffset / 2;
+ },
+ renderRelationPath: renderRelationPath,
+ renderConnectionPath: renderConnectionPath
+ },
+ vnffgd: {
+ list: [],
+ width: containerWidth,
+ height: containerHeight,
+ top(container, layouts) {
+ const positionIndex = layouts.nsd.list.length + this.list.length;
+ return (positionIndex * (this.height * 1.5)) + 120;
+ },
+ left(container, layouts) {
+ return 10;
+ },
+ renderRelationPath: renderRelationPath,
+ renderConnectionPath: renderConnectionPath
+ },
+ vnfd: {
+ list: [],
+ width: containerWidth,
+ height: containerHeight,
+ top(container) {
+ return (countAncestors(container) * 100) + 10;
+ },
+ left() {
+ const positionIndex = this.list.length;
+ return (positionIndex * (this.width * 1.5)) + leftOffset;
+ },
+ renderRelationPath: renderRelationPath,
+ renderConnectionPath: renderConnectionPath
+ },
+ 'constituent-vnfd': {
+ list: [],
+ width: containerWidth,
+ height: containerHeight,
+ top(container) {
+ return (countAncestors(container) * 100) + 10;
+ },
+ left() {
+ const positionIndex = this.list.length;
+ return (positionIndex * (this.width * 1.5)) + leftOffset;
+ },
+ renderRelationPath: renderRelationPath,
+ renderConnectionPath: renderConnectionPath
+ },
+ pnfd: {
+ list: [],
+ width: containerWidth,
+ height: containerHeight,
+ top(container) {
+ return (countAncestors(container) * 100) + 10;
+ },
+ left() {
+ const positionIndex = this.list.length;
+ return (positionIndex * (this.width * 1.5)) + leftOffset;
+ },
+ renderRelationPath: renderRelationPath,
+ renderConnectionPath: renderConnectionPath
+ },
+ vld: {
+ list: [],
+ width: containerWidth,
+ height: 38,
+ top(container) {
+ return (countAncestors(container) * 100) + 180;
+ },
+ left() {
+ const positionIndex = this.list.length;
+ const marginOffsetFactor = 1.5;
+ const gutterOffsetFactor = 1.5;
+ return (positionIndex * (this.width * gutterOffsetFactor)) + ((this.width * marginOffsetFactor) / 2) + leftOffset;
+ },
+ renderRelationPath: renderRelationPath,
+ renderConnectionPath: renderConnectionPath
+ },
+ 'internal-vld': {
+ list: [],
+ width: containerWidth,
+ height: 38,
+ top(container, containerLayouts) {
+ return (countAncestors(container) * 100) + 100;
+ },
+ left() {
+ const positionIndex = this.list.length;
+ const marginOffsetFactor = 1.5;
+ const gutterOffsetFactor = 1.5;
+ return (positionIndex * (this.width * gutterOffsetFactor)) + ((this.width * marginOffsetFactor) / 2) + leftOffset;
+ },
+ renderRelationPath: renderRelationPath,
+ renderConnectionPath: renderConnectionPath
+ },
+ vdu: {
+ list: [],
+ width: containerWidth,
+ height: containerHeight,
+ gutter: 30,
+ top(container) {
+ return (countAncestors(container) * 100) + 10;
+ },
+ left(container) {
+ const positionIndex = this.list.length;
+ return (positionIndex * (this.width * 1.5)) + leftOffset;
+ },
+ renderRelationPath: renderRelationPath,
+ renderConnectionPath: renderConnectionPath
+ }
+ };
+
+ function getConnectionPointEdges() {
+
+ // 1. create a lookup map to find a connection-point by it's key
+ const connectionPointMap = {};
+ containerList.filter(d => d.connectionPoint).reduce((result, container) => {
+ return container.connectionPoint.reduce((result, connectionPoint) => {
+ result[connectionPoint.key] = connectionPoint;
+ connectionPoint.uiState.hasConnection = false;
+ return result;
+ }, result);
+ }, connectionPointMap);
+
+ // 2. determine position of the connection-point and connection-point-ref (they are the same)
+ const connectionPointRefList = [];
+ containerList.filter(container => container.connection).forEach(container => {
+ container.uiState.hasConnection = false;
+ container.connection.filter(d => d.key).forEach(cpRef => {
+ try {
+ const source = connectionPointMap[cpRef.key];
+ const destination = container;
+ source.uiState.hasConnection = true;
+ destination.uiState.hasConnection = true;
+ const edgeStateMachine = math.upperLowerEdgeLocation;
+ // angle is used to determine location top, bottom, right, left
+ const angle = math.angleBetweenPositions(source.parent.position, destination.position);
+ // distance is used to determine order of the connection points
+ const distance = math.distanceBetweenPositions(source.parent.position, destination.position);
+ cpRef.location = source.location = edgeStateMachine(angle);
+ source.edgeAngle = angle;
+ if (destination.type === 'vdu') {
+ source.edgeLength = Math.max(source.edgeLength || 0, distance);
+ }
+ // warn assigning same instance (e.g. pass by reference) so that changes will reflect thru
+ cpRef.position = source.position;
+ connectionPointRefList.push(cpRef);
+ } catch(e) {
+ return;
+ }
+ });
+ });
+
+ // 3. update the connection-point/-ref location based on the angle of the path
+ containerList.filter(d => d.connectionPoint).forEach(container => {
+ // group the connectors by their location and then update their position coordinates accordingly
+ const connectionPoints = container.connectionPoint.sort((a, b) => b.edgeLength - a.edgeLength);
+ const locationIndexCounters = {};
+ connectionPoints.forEach(connectionPoint => {
+ // location index is used to calculate the actual position where the path will terminate on the container
+ const location = connectionPoint.location;
+ const locationIndex = locationIndexCounters[location] || (locationIndexCounters[location] = 0);
+ connectionPoint.positionIndex = locationIndex;
+ if (/left/.test(location)) {
+ connectionPoint.position.moveLeft(connectionPoint.parent.position.left + 5 + ((connectionPoint.width + 1) * locationIndex));
+ } else {
+ connectionPoint.position.moveRight(connectionPoint.parent.position.right - 15 - ((connectionPoint.width + 1) * locationIndex));
+ }
+ if (/top/.test(location)) {
+ connectionPoint.position.moveTop(connectionPoint.parent.position.top - connectionPoint.height);
+ } else {
+ connectionPoint.position.moveTop(connectionPoint.parent.position.bottom);
+ }
+ locationIndexCounters[location] = locationIndex + 1;
+ });
+ });
+
+ return connectionPointRefList;
+
+ }
+
+ function drawConnectionPointsAndPaths(graph, connectionPointRefs) {
+
+ const paths = graph.paths.selectAll('.connection').data(connectionPointRefs, DescriptorModelFactory.containerIdentity);
+
+ paths.enter().append('path');
+
+ paths.attr({
+ 'data-uid': d => {
+ return d.uid;
+ },
+ 'class': d => {
+ return 'connection between-' + d.parent.type + '-and-' + d.type;
+ },
+ 'stroke-width': 5,
+ stroke: ColorGroups.vld.primary,
+ fill: 'transparent',
+ d: edge => {
+ const layout = containerLayoutInfo[edge.parent.type];
+ return layout.renderConnectionPath(edge, containerLayoutInfo);
+ }
+ }).on('cut', (container) => {
+
+ let success = false;
+
+ if (container && container.remove) {
+ success = container.remove();
+ }
+
+ if (success) {
+ CatalogItemsActions.catalogItemDescriptorChanged.defer(container.getRoot());
+ } else {
+ d3.event.preventDefault();
+ }
+
+ d3.event.stopPropagation();
+
+ });
+
+ paths.exit().remove();
+
+ const symbolSize = props.connectionPointSize;
+ const connectionPointSymbol = d3.svg.symbol().type('square').size(symbolSize);
+ const internalConnectionPointSymbolBottom = d3.svg.symbol().type('triangle-down').size(symbolSize);
+ const internalConnectionPointSymbolTop = d3.svg.symbol().type('triangle-up').size(symbolSize);
+
+ const connectors = containerList.filter(d => d.connectors).reduce((result, container) => {
+ return container.connectors.reduce((result, connector) => {
+ result.add(connector);
+ return result;
+ }, result);
+ }, new Set());
+
+ const points = graph.connectorsGroup.selectAll('.connector').data(Array.from(connectors), DescriptorModelFactory.containerIdentity);
+
+ points.enter().append('path');
+
+ points.attr({
+ 'data-uid': d => d.uid,
+ 'data-key': d => d.key,
+ 'data-cp-number': d => d.uiState.cpNumber,
+ 'class': d => {
+ return ClassNames('connector', d.type, d.parent.type, {
+ '-is-connected': d.uiState.hasConnection,
+ '-is-not-connected': !d.uiState.hasConnection
+ });
+ },
+ 'data-tip': d => {
+ const info = d.displayData;
+ return Object.keys(info).reduce((r, key) => {
+ if (info[key]) {
+ return r + `<div class="${key}"><i>${key}</i><val>${info[key]}</val></div>`;
+ }
+ return r;
+ }, '');
+ },
+ 'data-tip-offset': d => {
+ if (d.type === 'internal-connection-point') {
+ return '{"top": -7, "left": -9}';
+ }
+ return '{"top": -5, "left": -5}';
+ },
+ transform: d => {
+ const point = d.position.centerPoint();
+ return 'translate(' + (point.x) + ', ' + (point.y) + ')';
+ },
+ d: d => {
+ if (d.type === 'connection-point') {
+ return connectionPointSymbol();
+ }
+ if (/top/.test(d.location)) {
+ return internalConnectionPointSymbolTop();
+ }
+ return internalConnectionPointSymbolBottom();
+ }
+ }).on('cut', (container) => {
+
+ let success = false;
+
+ if (container && container.remove) {
+ success = container.remove();
+ }
+
+ if (success) {
+ CatalogItemsActions.catalogItemDescriptorChanged.defer(container.getRoot());
+ } else {
+ d3.event.preventDefault();
+ }
+
+ d3.event.stopPropagation();
+
+ }).on('mouseenter', () => {
+ TooltipManager.showTooltip(d3.event.target);
+ });
+
+ points.exit().remove();
+
+ const test = new GraphConnectionPointNumber(graph);
+ test.addContainers(Array.from(connectors));
+ test.render();
+ }
+
+ function drawRelationPointsAndPaths (graph, relationEdges) {
+
+ const paths = graph.paths.selectAll('.relation').data(relationEdges, DescriptorModelFactory.containerIdentity);
+
+ paths.enter().append('path')
+ .attr({
+ 'class': d => {
+ return ClassNames('relation', d.type, {'-is-selected': d.uiState && SelectionManager.isSelected(d) /*d.uiState && d.uiState.selected*/});
+ },
+ stroke: 'red',
+ fill: 'transparent',
+ 'marker-start': 'url(#relation-marker-end)',
+ 'marker-end': 'url(#relation-marker-end)'
+ });
+
+ paths.attr({
+ d: d => {
+ const src = d;
+ const dst = d.parent;
+ const layout = containerLayoutInfo[src.type];
+ return layout.renderRelationPath(src, dst, containerLayoutInfo);
+ }
+ });
+
+ paths.exit().remove();
+
+ }
+
+ function updateContainerPosition(graph, container, layout) {
+ // use the provided layout to generate the position coordinates
+ const position = container.position;
+ position.top = layout.top(container, containerLayoutInfo) + marginTop;
+ position.left = layout.left(container, containerLayoutInfo) + marginLeft;
+ position.width = layout.width;
+ position.height = layout.height;
+ // cache the default layout position which may be needed by the layout
+ // of children elements that have not been positioned by the user
+ container.uiState.defaultLayoutPosition = position.value();
+ const savedContainerPosition = graph.lookupSavedContainerPosition(container);
+ if (savedContainerPosition) {
+ // set the container position with the saved position coordinates
+ container.setPosition(savedContainerPosition);
+ }
+ if (container.uiState.dropCoordinates) {
+ const rect = graph.svg.node().getBoundingClientRect();
+ const top = container.uiState.dropCoordinates.clientY - (position.height / 2) - rect.top;
+ const left = container.uiState.dropCoordinates.clientX - (position.width / 2) - rect.left;
+ container.position.move(Math.max(props.padding, left), Math.max(props.padding, top));
+ graph.saveContainerPosition(container);
+ delete container.uiState.dropCoordinates;
+ } else {
+ graph.saveContainerPosition(container);
+ }
+ }
+
+ return {
+
+ addContainers(containers) {
+
+ const layout = this;
+
+ //containers = containers.filter(d => containerLayouts[d.type]);
+
+ const graphSize = {
+ width: 0,
+ height: 0
+ };
+
+ containers.forEach(container => {
+ containerList.push(container);
+ });
+
+ containers.forEach(container => {
+ const layout = containerLayoutInfo[container.type];
+ if (!layout) {
+ return
+ //throw new ReferenceError('unknown container type: ' + container.type);
+ }
+ updateContainerPosition(graph, container, layout);
+ layout.list.push(container);
+ graphSize.width = Math.max(graphSize.width, container.position.right, props.width);
+ graphSize.height = Math.max(graphSize.height, container.position.bottom, props.height);
+ });
+
+ graph.svg.attr({
+ width: graphSize.width + props.width,
+ height: graphSize.height + props.height
+ });
+
+ const uiTransientState = {
+ isDragging: false,
+ dragStartInfo: [0, 0]
+ };
+
+ // todo extract drag behavior into class DescriptorGraphDrag
+
+ const drag = this.drag = d3.behavior.drag()
+ .origin(function(d) { return d; })
+ .on('drag.graph', function (d) {
+ uiTransientState.isDragging = true;
+ const mouse = d3.mouse(graph.g.node());
+ const offset = uiTransientState.dragStartInfo;
+ const newTopEdge = snapTo(mouse[1] - offset[1]);
+ const newLeftEdge = snapTo(mouse[0] - offset[0]);
+ if (d.position.left === newLeftEdge && d.position.top === newTopEdge) {
+ // do not redraw since we are not moving the container
+ return;
+ }
+ d.position.move(newLeftEdge, newTopEdge);
+ graph.saveContainerPosition(d);
+ const connectionPointRefs = getConnectionPointEdges();
+ d3.select(this).attr({
+ transform: () => {
+ const x = d.position.left;
+ const y = d.position.top;
+ return 'translate(' + x + ', ' + y + ')';
+ }
+ });
+ requestAnimationFrame(() => {
+ drawConnectionPointsAndPaths(graph, connectionPointRefs);
+ layout.renderers.forEach(d => d.render());
+ });
+ }).on('dragend.graph', () => {
+ // d3 fires a drag-end event on mouse up, even if just clicking
+ if (uiTransientState.isDragging) {
+ uiTransientState.isDragging = false;
+ CatalogItemsActions.catalogItemMetaDataChanged(graph.containers[0].model);
+ d3.select(this).on('.graph', null);
+ }
+ }).on('dragstart.graph', function (d) {
+ // the x, y offset of the mouse from the container's left, top
+ uiTransientState.dragStartInfo = d3.mouse(this);
+ });
+
+ this.renderers = [GraphVirtualLink, GraphNetworkService, GraphForwardingGraph, GraphVirtualNetworkFunction, GraphConstituentVnfd, GraphVirtualDeploymentUnit, GraphRecordServicePath, GraphInternalVirtualLink].map(layout => {
+ const container = new layout(graph, props);
+ const layoutInfo = containerLayoutInfo[container.classType && container.classType.type];
+ if (layoutInfo) {
+ container.props.descriptorWidth = layoutInfo.width;
+ container.props.descriptorHeight = layoutInfo.height;
+ }
+ container.dragHandler = drag;
+ container.addContainers(containerList);
+ return container;
+ });
+
+ },
+
+ render(graph, updateCallback = () => {}) {
+ const connectionPointRefs = getConnectionPointEdges();
+ requestAnimationFrame(() => {
+ drawConnectionPointsAndPaths(graph, connectionPointRefs);
+ this.renderers.forEach(d => d.render());
+ updateCallback();
+ });
+ }
+
+ };
+
+}
\ No newline at end of file