[AAI-92 Amsterdam] Update license
[aai/sparky-fe.git] / src / generic-components / graph / ForceDirectedGraph.jsx
1 /*
2  * ============LICENSE_START=======================================================
3  * org.onap.aai
4  * ================================================================================
5  * Copyright © 2017 AT&T Intellectual Property. All rights reserved.
6  * Copyright © 2017 Amdocs
7  * ================================================================================
8  * Licensed under the Apache License, Version 2.0 (the "License");
9  * you may not use this file except in compliance with the License.
10  * You may obtain a copy of the License at
11  *
12  *       http://www.apache.org/licenses/LICENSE-2.0
13  *
14  * Unless required by applicable law or agreed to in writing, software
15  * distributed under the License is distributed on an "AS IS" BASIS,
16  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17  * See the License for the specific language governing permissions and
18  * limitations under the License.
19  * ============LICENSE_END=========================================================
20  *
21  * ECOMP is a trademark and service mark of AT&T Intellectual Property.
22  */
23 import {drag} from 'd3-drag';
24 import {forceSimulation, forceLink, forceManyBody, forceCenter} from 'd3-force';
25 import {interpolateNumber} from 'd3-interpolate';
26 import {select, event as currentEvent} from 'd3-selection';
27 import React, {Component, PropTypes} from 'react';
28 import {interval, now} from 'd3-timer';
29 import {zoom, zoomIdentity} from 'd3-zoom';
30 import NodeConstants from './NodeVisualElementConstants.js';
31
32 import {simulationKeys} from './ForceDefinitions.js';
33 import NodeFactory from './NodeFactory.js';
34 import NodeVisualElementFactory from './NodeVisualElementFactory.js';
35
36 class ForceDirectedGraph extends Component {
37   static propTypes = {
38     viewWidth: PropTypes.number,
39     viewHeight: PropTypes.number,
40     graphData: PropTypes.object,
41     nodeIdKey: PropTypes.string,
42     linkIdKey: PropTypes.string,
43     nodeSelectedCallback: PropTypes.func,
44     nodeButtonSelectedCallback: PropTypes.func,
45     currentlySelectedNodeView: PropTypes.string
46   };
47   
48   static defaultProps = {
49     viewWidth: 0,
50     viewHeight: 0,
51     graphData: {
52       graphCounter: -1, nodeDataArray: [], linkDataArray: [], graphMeta: {}
53     },
54     nodeIdKey: '',
55     linkIdKey: '',
56     nodeSelectedCallback: undefined,
57     nodeButtonSelectedCallback: undefined,
58     currentlySelectedNodeView: ''
59   };
60   
61   constructor(props) {
62     super(props);
63     
64     this.state = {
65       nodes: [], links: [], mainGroupTransform: zoomIdentity
66     };
67     
68     this.updateSimulationForce = this.updateSimulationForce.bind(this);
69     this.resetTransform = this.resetTransform.bind(this);
70     this.applyBufferDataToState = this.applyBufferDataToState.bind(this);
71     this.createNodePropForState = this.createNodePropForState.bind(this);
72     this.createLinkPropForState = this.createLinkPropForState.bind(this);
73     this.startSimulation = this.startSimulation.bind(this);
74     this.simulationComplete = this.simulationComplete.bind(this);
75     this.simulationTick = this.simulationTick.bind(this);
76     this.nodeSelected = this.nodeSelected.bind(this);
77     this.onZoom = this.onZoom.bind(this);
78     this.onGraphDrag = this.onGraphDrag.bind(this);
79     this.onNodeDrag = this.onNodeDrag.bind(this);
80     this.addNodeInterpolator = this.addNodeInterpolator.bind(this);
81     this.runInterpolators = this.runInterpolators.bind(this);
82     
83     this.nodeBuffer = [];
84     this.linkBuffer = [];
85     this.nodeDatum = [];
86     this.nodeButtonDatum = [];
87     this.nodeFactory = new NodeFactory();
88     this.visualElementFactory = new NodeVisualElementFactory();
89     
90     this.isGraphMounted = false;
91     
92     this.listenerGraphCounter = -1;
93     this.nodeIndexTracker = new Map();
94     this.interpolators = new Map();
95     this.areInterpolationsRunning = false;
96     
97     this.newNodeSelected = true;
98     this.currentlySelectedNodeButton = undefined;
99     
100     this.intervalTimer = interval(this.applyBufferDataToState, simulationKeys.DATA_COPY_INTERVAL);
101     this.intervalTimer.stop();
102     
103     this.interpolationTimer = interval(this.runInterpolators, simulationKeys.DATA_COPY_INTERVAL);
104     this.interpolationTimer.stop();
105     
106     this.simulation = forceSimulation();
107     this.simulation.on('end', this.simulationComplete);
108     this.simulation.stop();
109     
110     this.svgZoom =
111       zoom().scaleExtent([NodeConstants.SCALE_EXTENT_MIN, NodeConstants.SACEL_EXTENT_MAX]);
112     this.svgZoom.clickDistance(2);
113     this.nodeDrag = drag().clickDistance(2);
114     
115     this.updateSimulationForce();
116     
117     if (props.graphData) {
118       if (props.graphData.graphCounter !== -1) {
119         this.startSimulation(props.graphData);
120       }
121     }
122   }
123   
124   componentDidMount() {
125     this.isGraphMounted = true;
126   }
127   
128   componentWillReceiveProps(nextProps) {
129     if (nextProps.graphData.graphCounter !== this.props.graphData.graphCounter) {
130       this.listenerGraphCounter = this.props.graphData.graphCounter;
131       this.newNodeSelected = true;
132       this.resetTransform();
133       this.startSimulation(nextProps.graphData);
134     }
135   }
136   
137   componentDidUpdate(prevProps) {
138     let hasNewGraphDataRendered = (prevProps.graphData.graphCounter ===
139     this.props.graphData.graphCounter);
140     let shouldAttachListeners = (this.listenerGraphCounter !== this.props.graphData.graphCounter);
141     let nodeCount = this.state.nodes.length;
142     
143     if (nodeCount > 0) {
144       if (hasNewGraphDataRendered && shouldAttachListeners) {
145         
146         let nodes = select('.fdgMainSvg').select('.fdgMainG')
147                                          .selectAll('.aai-entity-node')
148                                          .data(this.nodeDatum);
149         
150         nodes.on('click', (d) => {
151           this.nodeSelected(d);
152         });
153         
154         nodes.call(this.nodeDrag.on('drag', (d) => {
155           let xAndY = [currentEvent.x, currentEvent.y];
156           this.onNodeDrag(d, xAndY);
157         }));
158         
159         let mainSVG = select('.fdgMainSvg');
160         let mainView = mainSVG.select('.fdgMainView');
161         this.svgZoom.transform(mainSVG, zoomIdentity);
162         this.svgZoom.transform(mainView, zoomIdentity);
163         
164         mainSVG.call(this.svgZoom.on('zoom', () => { // D3 Zoom also handles panning
165           this.onZoom(currentEvent.transform);
166         })).on('dblclick.zoom', null); // Ignore the double-click zoom event
167         
168         this.listenerGraphCounter = this.props.graphData.graphCounter;
169       }
170     }
171   }
172   
173   componentWillUnmount() {
174     this.isGraphMounted = false;
175     
176     let nodes = select('.fdgMainSvg').select('.fdgMainG')
177                                      .selectAll('.aai-entity-node');
178     let nodeButtons = nodes.selectAll('.node-button');
179     
180     nodes.on('click', null);
181     nodeButtons.on('click', null);
182     
183     let mainSVG = select('.fdgMainSvg');
184     
185     mainSVG.call(this.svgZoom.on('zoom', null)).on('dblclick.zoom', null);
186     mainSVG.call(drag().on('drag', null));
187   }
188   
189   updateSimulationForce() {
190     this.simulation.force('link', forceLink());
191     this.simulation.force('link').id((d) => {
192       return d.id;
193     });
194     this.simulation.force('link').strength(0.3);
195     this.simulation.force('link').distance(100);
196     
197     this.simulation.force('charge', forceManyBody());
198     this.simulation.force('charge').strength(-1250);
199     this.simulation.alpha(1);
200     
201     this.simulation.force('center',
202       forceCenter(this.props.viewWidth / 2, this.props.viewHeight / 2));
203   }
204   
205   resetTransform() {
206     if (this.isGraphMounted) {
207       this.setState(() => {
208         return {
209           mainGroupTransform: zoomIdentity
210         };
211       });
212     }
213   }
214   
215   applyBufferDataToState() {
216     this.nodeIndexTracker.clear();
217     
218     let newNodes = [];
219     this.nodeBuffer.map((node, i) => {
220       let nodeProps = this.createNodePropForState(node);
221       
222       if (nodeProps.meta.nodeMeta.className === NodeConstants.SELECTED_NODE_CLASS_NAME ||
223         nodeProps.meta.nodeMeta.className === NodeConstants.SELECTED_SEARCHED_NODE_CLASS_NAME) {
224         
225         this.nodeButtonDatum[0].data = nodeProps.meta;
226         
227         nodeProps = {
228           ...nodeProps,
229           buttons: [this.nodeButtonDatum[0].isSelected]
230         };
231       }
232       
233       newNodes.push(this.nodeFactory.buildNode(nodeProps.meta.nodeMeta.className, nodeProps));
234       
235       this.nodeIndexTracker.set(node.id, i);
236     });
237     
238     let newLinks = [];
239     this.linkBuffer.map((link) => {
240       let key = link.id;
241       let linkProps = this.createLinkPropForState(link);
242       newLinks.push(this.visualElementFactory.createSvgLine(linkProps, key));
243     });
244     
245     if (this.isGraphMounted) {
246       this.setState(() => {
247         return {
248           nodes: newNodes, links: newLinks
249         };
250       });
251     }
252   }
253   
254   createNodePropForState(nodeData) {
255     return {
256       renderProps: {
257         key: nodeData.id, x: nodeData.x, y: nodeData.y
258       }, meta: {
259         ...nodeData
260       }
261     };
262   }
263   
264   createLinkPropForState(linkData) {
265     return {
266       className: 'aai-entity-link',
267       x1: linkData.source.x,
268       y1: linkData.source.y,
269       x2: linkData.target.x,
270       y2: linkData.target.y
271     };
272   }
273   
274   startSimulation(graphData) {
275     this.nodeFactory.setNodeMeta(graphData.graphMeta);
276     
277     // Experiment with removing length = 0... might not be needed as new array
278     // assignment will likely destroy old reference
279     this.nodeBuffer.length = 0;
280     this.nodeBuffer = Array.from(graphData.nodeDataArray);
281     this.linkBuffer.length = 0;
282     this.linkBuffer = Array.from(graphData.linkDataArray);
283     this.nodeDatum.length = 0;
284     this.nodeDatum = Array.from(graphData.nodeDataArray);
285     
286     this.nodeButtonDatum.length = 0;
287     
288     let isNodeDetailsSelected = true;
289     this.nodeButtonDatum.push({
290       name: NodeConstants.ICON_ELLIPSES, isSelected: isNodeDetailsSelected
291     });
292     
293     if (isNodeDetailsSelected) {
294       this.currentlySelectedNodeButton = NodeConstants.ICON_ELLIPSES;
295     }
296     
297     this.updateSimulationForce();
298     
299     this.simulation.nodes(this.nodeBuffer);
300     this.simulation.force('link').links(this.linkBuffer);
301     this.simulation.on('tick', this.simulationTick);
302     this.simulation.restart();
303   }
304   
305   simulationComplete() {
306     this.intervalTimer.stop();
307     this.applyBufferDataToState();
308   }
309   
310   simulationTick() {
311     this.intervalTimer.restart(this.applyBufferDataToState, simulationKeys.DATA_COPY_INTERVAL);
312     this.simulation.on('tick', null);
313   }
314   
315   nodeSelected(datum) {
316     if (this.props.nodeSelectedCallback) {
317       this.props.nodeSelectedCallback(datum);
318     }
319     
320     let didUpdateNew = false;
321     let didUpdatePrevious = false;
322     let isSameNodeSelected = true;
323     
324     // Check to see if a default node was previously selected
325     let selectedDefaultNode = select('.fdgMainSvg').select('.fdgMainG')
326                                                    .selectAll('.aai-entity-node')
327                                                    .filter('.selected-node');
328     if (!selectedDefaultNode.empty()) {
329       if (selectedDefaultNode.datum().id !== datum.id) {
330         this.nodeBuffer[selectedDefaultNode.datum().index].nodeMeta.className =
331           NodeConstants.GENERAL_NODE_CLASS_NAME;
332         didUpdatePrevious = true;
333         isSameNodeSelected = false;
334       }
335     }
336     
337     // Check to see if a searched node was previously selected
338     let selectedSearchedNode = select('.fdgMainSvg').select('.fdgMainG')
339                                                     .selectAll('.aai-entity-node')
340                                                     .filter('.selected-search-node');
341     if (!selectedSearchedNode.empty()) {
342       if (selectedSearchedNode.datum().id !== datum.id) {
343         this.nodeBuffer[selectedSearchedNode.datum().index].nodeMeta.className =
344           NodeConstants.SEARCHED_NODE_CLASS_NAME;
345         didUpdatePrevious = true;
346         isSameNodeSelected = false;
347       }
348     }
349     
350     if (!isSameNodeSelected) {
351       let newlySelectedNode = select('.fdgMainSvg').select('.fdgMainG')
352                                                    .selectAll('.aai-entity-node')
353                                                    .filter((d) => {
354                                                      return (datum.id === d.id);
355                                                    });
356       if (!newlySelectedNode.empty()) {
357         
358         if (newlySelectedNode.datum().nodeMeta.searchTarget) {
359           this.nodeBuffer[newlySelectedNode.datum().index].nodeMeta.className =
360             NodeConstants.SELECTED_SEARCHED_NODE_CLASS_NAME;
361         } else {
362           this.nodeBuffer[newlySelectedNode.datum().index].nodeMeta.className =
363             NodeConstants.SELECTED_NODE_CLASS_NAME;
364         }
365         didUpdateNew = true;
366       }
367     }
368     
369     if (didUpdatePrevious && didUpdateNew) {
370       this.newNodeSelected = true;
371       this.applyBufferDataToState();
372     }
373   }
374
375   onZoom(eventTransform) {
376     if (this.isGraphMounted) {
377       this.setState(() => {
378         return {
379           mainGroupTransform: eventTransform
380         };
381       });
382     }
383   }
384   
385   onGraphDrag(xAndYCoords) {
386     let translate = `translate(${xAndYCoords.x}, ${xAndYCoords.y})`;
387     let oldTransform = this.state.mainGroupTransform;
388     if (this.isGraphMounted) {
389       this.setState(() => {
390         return {
391           ...oldTransform, translate
392         };
393       });
394     }
395   }
396   
397   onNodeDrag(datum, xAndYCoords) {
398     let nodeIndex = this.nodeIndexTracker.get(datum.id);
399     if (this.nodeBuffer[nodeIndex]) {
400       this.nodeBuffer[nodeIndex].x = xAndYCoords[0];
401       this.nodeBuffer[nodeIndex].y = xAndYCoords[1];
402       this.applyBufferDataToState();
403     }
404   }
405   
406   addNodeInterpolator(nodeId, key, startingValue, endingValue, duration) {
407     let numberInterpolator = interpolateNumber(startingValue, endingValue);
408     let timeNow = now();
409     let interpolationObject = {
410       nodeId: nodeId, key: key, duration: duration, timeCreated: timeNow, method: numberInterpolator
411     };
412     this.interpolators.set(nodeId, interpolationObject);
413     
414     if (!this.areInterpolationsRunning) {
415       this.interpolationTimer.restart(this.runInterpolators, simulationKeys.DATA_COPY_INTERVAL);
416       this.areInterpolationsRunning = true;
417     }
418   }
419   
420   runInterpolators() {
421     // If we have no more interpolators to run then shut'r down!
422     if (this.interpolators.size === 0) {
423       this.interpolationTimer.stop();
424       this.areInterpolationsRunning = false;
425     }
426     
427     let iterpolatorsComplete = [];
428     // Apply interpolation values
429     this.interpolators.forEach((interpolator) => {
430       let nodeIndex = this.nodeIndexTracker.get(interpolator.nodeId);
431       if (nodeIndex) {
432         let elapsedTime = now() - interpolator.timeCreated;
433         // Normalize t as D3's interpolateNumber needs a value between 0 and 1
434         let t = elapsedTime / interpolator.duration;
435         if (t >= 1) {
436           t = 1;
437           iterpolatorsComplete.push(interpolator.nodeId);
438         }
439         this.nodeBuffer[nodeIndex][interpolator.key] = interpolator.method(t);
440       }
441     });
442     
443     // Remove any interpolators that are complete
444     if (iterpolatorsComplete.length > 0) {
445       for (let i = 0; i < iterpolatorsComplete.length; i++) {
446         this.interpolators.delete(iterpolatorsComplete[i]);
447       }
448     }
449     
450     this.applyBufferDataToState();
451   }
452   
453   render() {
454     // We will be using these values veru shortly, commenting out for eslint
455     // reasons so we can build for PV let {viewWidth, viewHeight} = this.props;
456     let {nodes, links, mainGroupTransform} = this.state;
457     
458     return (
459       <div className='ts-force-selected-graph'>
460         <svg className={'fdgMainSvg'} width='100%' height='100%'>
461           <rect className={'fdgMainView'} x='0.5' y='0.5' width='99%'
462                 height='99%' fill='none'/>
463           <filter id='selected-node-drop-shadow'>
464             <feGaussianBlur in='SourceAlpha' stdDeviation='2.2'/>
465             <feOffset dx='-1' dy='1' result='offsetblur'/>
466             <feFlood floodColor='rgba(0,0,0,0.5)'/>
467             <feComposite in2='offsetblur' operator='in'/>
468             <feMerge>
469               <feMergeNode/>
470               <feMergeNode in='SourceGraphic'/>
471             </feMerge>
472           </filter>
473           <g className={'fdgMainG'} transform={mainGroupTransform}>
474             {links}
475             {nodes}
476           </g>
477         </svg>
478       </div>
479     );
480   }
481   
482   static graphCounter = 0;
483   
484   static generateNewProps(nodeArray, linkArray, metaData) {
485     ForceDirectedGraph.graphCounter += 1;
486     return {
487       graphCounter: ForceDirectedGraph.graphCounter,
488       nodeDataArray: nodeArray,
489       linkDataArray: linkArray,
490       graphMeta: metaData
491     };
492   }
493 }
494
495 export default ForceDirectedGraph;