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