00c2575ebcd51eaf1aaeede23e2e42184d1e4e5c
[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.resetState = this.resetState.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.nodeButtonSelected = this.nodeButtonSelected.bind(this);
81     this.onZoom = this.onZoom.bind(this);
82     this.onGraphDrag = this.onGraphDrag.bind(this);
83     this.onNodeDrag = this.onNodeDrag.bind(this);
84     this.addNodeInterpolator = this.addNodeInterpolator.bind(this);
85     this.runInterpolators = this.runInterpolators.bind(this);
86
87     this.nodeBuffer = [];
88     this.linkBuffer = [];
89     this.nodeDatum = [];
90     this.nodeButtonDatum = [];
91     this.nodeFactory = new NodeFactory();
92     this.visualElementFactory = new NodeVisualElementFactory();
93
94     this.isGraphMounted = false;
95
96     this.listenerGraphCounter = -1;
97     this.nodeIndexTracker = new Map();
98     this.interpolators = new Map();
99     this.areInterpolationsRunning = false;
100
101     this.newNodeSelected = true;
102     this.currentlySelectedNodeButton = undefined;
103
104     this.intervalTimer = interval(this.applyBufferDataToState, simulationKeys.DATA_COPY_INTERVAL);
105     this.intervalTimer.stop();
106
107     this.interpolationTimer = interval(this.runInterpolators, simulationKeys.DATA_COPY_INTERVAL);
108     this.interpolationTimer.stop();
109
110     this.simulation = forceSimulation();
111     this.simulation.on('end', this.simulationComplete);
112     this.simulation.stop();
113
114     this.svgZoom =
115       zoom().scaleExtent([NodeConstants.SCALE_EXTENT_MIN, NodeConstants.SACEL_EXTENT_MAX]);
116     this.svgZoom.clickDistance(2);
117     this.nodeDrag = drag().clickDistance(2);
118
119     this.updateSimulationForce();
120     // Temporary code until backend supports NOT displaying the button in the response.
121     if(props.dataOverlayButtons.length === 1) {
122       this.hideButton = true;
123     } else {
124       this.hideButton  = false;
125     }
126     if (props.graphData) {
127       if (props.graphData.graphCounter !== -1) {
128         this.startSimulation(props.graphData, props.currentlySelectedNodeView, props.dataOverlayButtons);
129       }
130     }
131   }
132
133   componentDidMount() {
134     this.isGraphMounted = true;
135   }
136
137   componentWillReceiveProps(nextProps) {
138     if (nextProps.graphData.graphCounter !== this.props.graphData.graphCounter) {
139       this.listenerGraphCounter = this.props.graphData.graphCounter;
140       this.newNodeSelected = true;
141       this.resetState();
142       this.startSimulation(nextProps.graphData, nextProps.currentlySelectedNodeView, nextProps.dataOverlayButtons);
143     }
144   }
145
146
147   componentDidUpdate(prevProps) {
148     let hasNewGraphDataRendered = (prevProps.graphData.graphCounter ===
149     this.props.graphData.graphCounter);
150     let shouldAttachListeners = (this.listenerGraphCounter !== this.props.graphData.graphCounter);
151     let nodeCount = this.state.nodes.length;
152
153     if (nodeCount > 0) {
154       if (hasNewGraphDataRendered && shouldAttachListeners) {
155         let nodes = select('.fdgMainSvg').select('.fdgMainG')
156                                          .selectAll('.aai-entity-node')
157                                          .data(this.nodeDatum);
158         nodes.on('click', (d) => {
159           this.nodeSelected(d);
160         });
161
162         nodes.call(this.nodeDrag.on('drag', (d) => {
163           let xAndY = [currentEvent.x, currentEvent.y];
164           this.onNodeDrag(d, xAndY);
165         }));
166
167         let mainSVG = select('.fdgMainSvg');
168         let mainView = mainSVG.select('.fdgMainView');
169         this.svgZoom.transform(mainSVG, zoomIdentity);
170         this.svgZoom.transform(mainView, zoomIdentity);
171
172         mainSVG.call(this.svgZoom.on('zoom', () => { // D3 Zoom also handles panning
173           this.onZoom(currentEvent.transform);
174         })).on('dblclick.zoom', null); // Ignore the double-click zoom event
175
176         this.listenerGraphCounter = this.props.graphData.graphCounter;
177       }
178
179       if (this.newNodeSelected) {
180         let nodeButtons = select('.fdgMainSvg').select('.fdgMainG')
181                                                .selectAll('.aai-entity-node')
182                                                .selectAll('.node-button')
183                                                .data(this.nodeButtonDatum);
184         if (!nodeButtons.empty()) {
185           nodeButtons.on('click', (d) => {
186             this.nodeButtonSelected(d);
187           });
188           if (hasNewGraphDataRendered && shouldAttachListeners) {
189             this.newNodeSelected = false;
190           }
191         }
192       }
193     }
194   }
195
196   componentWillUnmount() {
197     this.isGraphMounted = false;
198
199     let nodes = select('.fdgMainSvg').select('.fdgMainG')
200                                      .selectAll('.aai-entity-node');
201     let nodeButtons = nodes.selectAll('.node-button');
202
203     nodes.on('click', null);
204     nodeButtons.on('click', null);
205
206     let mainSVG = select('.fdgMainSvg');
207
208     mainSVG.call(this.svgZoom.on('zoom', null)).on('dblclick.zoom', null);
209     mainSVG.call(drag().on('drag', null));
210   }
211
212   updateSimulationForce() {
213     this.simulation.force('link', forceLink());
214     this.simulation.force('link').id((d) => {
215       return d.id;
216     });
217     this.simulation.force('link').strength(0.3);
218     this.simulation.force('link').distance(100);
219
220     this.simulation.force('charge', forceManyBody());
221     this.simulation.force('charge').strength(-1250);
222     this.simulation.alpha(1);
223
224     this.simulation.force('center',
225       forceCenter(this.props.viewWidth / 2, this.props.viewHeight / 2));
226   }
227
228   resetState() {
229     if (this.isGraphMounted) {
230       this.setState(() => {
231         return {
232           mainGroupTransform: zoomIdentity,
233           nodes: [], links: []
234         };
235       });
236     }
237   }
238
239   applyBufferDataToState() {
240     this.nodeIndexTracker.clear();
241
242     let newNodes = [];
243     this.nodeBuffer.map((node, i) => {
244       let nodeProps = this.createNodePropForState(node);
245
246       if (nodeProps.meta.nodeMeta.className ===
247         NodeConstants.SELECTED_NODE_CLASS_NAME ||
248         nodeProps.meta.nodeMeta.className ===
249         NodeConstants.SELECTED_SEARCHED_NODE_CLASS_NAME) {
250
251         this.nodeButtonDatum[0].data = nodeProps.meta;
252         if(this.nodeButtonDatum.length > 1) {
253           this.nodeButtonDatum[1].data = nodeProps.meta;
254           nodeProps = {
255             ...nodeProps,
256             buttons: [this.nodeButtonDatum[0].isSelected, this.nodeButtonDatum[1].isSelected]
257           };
258         } else {
259           nodeProps = {
260             ...nodeProps,
261             buttons: [this.nodeButtonDatum[0].isSelected]
262           };
263         }
264       }
265
266       newNodes.push(this.nodeFactory.buildNode(nodeProps.meta.nodeMeta.className, nodeProps, this.hideButton));
267
268       this.nodeIndexTracker.set(node.id, i);
269     });
270
271     let newLinks = [];
272     this.linkBuffer.map((link) => {
273       let key = link.id;
274       let linkProps = this.createLinkPropForState(link);
275       newLinks.push(this.visualElementFactory.createSvgLine(linkProps, key));
276     });
277
278     if (this.isGraphMounted) {
279       this.setState(() => {
280         return {
281           nodes: newNodes, links: newLinks
282         };
283       });
284     }
285   }
286
287   createNodePropForState(nodeData) {
288     return {
289       renderProps: {
290         key: nodeData.id, x: nodeData.x, y: nodeData.y
291       }, meta: {
292         ...nodeData
293       }
294     };
295   }
296
297   createLinkPropForState(linkData) {
298     return {
299       className: 'aai-entity-link',
300       x1: linkData.source.x,
301       y1: linkData.source.y,
302       x2: linkData.target.x,
303       y2: linkData.target.y
304     };
305   }
306
307   startSimulation(graphData, currentView, overlayButtons) {
308     this.nodeFactory.setNodeMeta(graphData.graphMeta);
309
310     // Experiment with removing length = 0... might not be needed as new array
311     // assignment will likely destroy old reference
312     this.nodeBuffer.length = 0;
313     this.nodeBuffer = Array.from(graphData.nodeDataArray);
314     this.linkBuffer.length = 0;
315     this.linkBuffer = Array.from(graphData.linkDataArray);
316     this.nodeDatum.length = 0;
317     this.nodeDatum = Array.from(graphData.nodeDataArray);
318
319     this.nodeButtonDatum.length = 0;
320
321     let isNodeDetailsSelected = (currentView ===
322     overlayButtons[0] ||
323     currentView ===
324     '');
325     this.nodeButtonDatum.push({
326       name: NodeConstants.ICON_ELLIPSES, isSelected: isNodeDetailsSelected, overlayName: overlayButtons[0]
327     });
328
329
330     if(overlayButtons.length > 1 ) {
331       let isSecondButtonSelected = (currentView === overlayButtons[1]);
332
333       this.nodeButtonDatum.push({
334         name: NodeConstants.ICON_TRIANGLE_WARNING, isSelected: isSecondButtonSelected, overlayName: overlayButtons[1]
335       });
336     }
337
338
339     if (isNodeDetailsSelected) {
340       this.currentlySelectedNodeButton = NodeConstants.ICON_ELLIPSES;
341     } else {
342       this.currentlySelectedNodeButton = NodeConstants.ICON_TRIANGLE_WARNING;
343     }
344
345     this.updateSimulationForce();
346
347     this.simulation.nodes(this.nodeBuffer);
348     this.simulation.force('link').links(this.linkBuffer);
349     this.simulation.on('tick', this.simulationTick);
350     this.simulation.restart();
351   }
352
353   simulationComplete() {
354     this.intervalTimer.stop();
355     this.applyBufferDataToState();
356   }
357
358   simulationTick() {
359     this.intervalTimer.restart(this.applyBufferDataToState, simulationKeys.DATA_COPY_INTERVAL);
360     this.simulation.on('tick', null);
361   }
362
363   nodeSelected(datum) {
364     if (this.props.nodeSelectedCallback) {
365       this.props.nodeSelectedCallback(datum);
366     }
367
368     let didUpdateNew = false;
369     let didUpdatePrevious = false;
370     let isSameNodeSelected = true;
371
372     // Check to see if a default node was previously selected
373     let selectedDefaultNode = select('.fdgMainSvg').select('.fdgMainG')
374                                                    .selectAll('.aai-entity-node')
375                                                    .filter('.selected-node');
376     if (!selectedDefaultNode.empty()) {
377       if (selectedDefaultNode.datum().id !== datum.id) {
378         this.nodeBuffer[selectedDefaultNode.datum().index].nodeMeta.className =
379           NodeConstants.GENERAL_NODE_CLASS_NAME;
380         didUpdatePrevious = true;
381         isSameNodeSelected = false;
382       }
383     }
384
385     // Check to see if a searched node was previously selected
386     let selectedSearchedNode = select('.fdgMainSvg').select('.fdgMainG')
387                                                     .selectAll('.aai-entity-node')
388                                                     .filter('.selected-search-node');
389     if (!selectedSearchedNode.empty()) {
390       if (selectedSearchedNode.datum().id !== datum.id) {
391         this.nodeBuffer[selectedSearchedNode.datum().index].nodeMeta.className =
392           NodeConstants.SEARCHED_NODE_CLASS_NAME;
393         didUpdatePrevious = true;
394         isSameNodeSelected = false;
395       }
396     }
397
398     if (!isSameNodeSelected) {
399       let newlySelectedNode = select('.fdgMainSvg').select('.fdgMainG')
400                                                    .selectAll('.aai-entity-node')
401                                                    .filter((d) => {
402                                                      return (datum.id === d.id);
403                                                    });
404       if (!newlySelectedNode.empty()) {
405
406         if (newlySelectedNode.datum().nodeMeta.searchTarget) {
407           this.nodeBuffer[newlySelectedNode.datum().index].nodeMeta.className =
408             NodeConstants.SELECTED_SEARCHED_NODE_CLASS_NAME;
409         } else {
410           this.nodeBuffer[newlySelectedNode.datum().index].nodeMeta.className =
411             NodeConstants.SELECTED_NODE_CLASS_NAME;
412         }
413         didUpdateNew = true;
414       }
415     }
416
417     if (didUpdatePrevious && didUpdateNew) {
418       this.newNodeSelected = true;
419       this.applyBufferDataToState();
420     }
421   }
422
423   nodeButtonSelected(datum) {
424     if (this.props.nodeButtonSelectedCallback) {
425       let buttonClickEvent = {
426         buttonId: datum.overlayName
427       };
428       this.props.nodeButtonSelectedCallback(buttonClickEvent);
429     }
430
431     if (this.currentlySelectedNodeButton !== datum.name) {
432       if (datum.name === this.nodeButtonDatum[0].name) {
433         this.nodeButtonDatum[0].isSelected = true;
434         this.nodeButtonDatum[1].isSelected = false;
435       }
436       if (datum.name === this.nodeButtonDatum[1].name) {
437         this.nodeButtonDatum[0].isSelected = false;
438         this.nodeButtonDatum[1].isSelected = true;
439       }
440       this.currentlySelectedNodeButton = datum.name;
441       this.applyBufferDataToState();
442     }
443   }
444
445   onZoom(eventTransform) {
446     if (this.isGraphMounted) {
447       this.setState(() => {
448         return {
449           mainGroupTransform: eventTransform
450         };
451       });
452     }
453   }
454
455   onGraphDrag(xAndYCoords) {
456     let translate = `translate(${xAndYCoords.x}, ${xAndYCoords.y})`;
457     let oldTransform = this.state.mainGroupTransform;
458     if (this.isGraphMounted) {
459       this.setState(() => {
460         return {
461           ...oldTransform, translate
462         };
463       });
464     }
465   }
466
467   onNodeDrag(datum, xAndYCoords) {
468     let nodeIndex = this.nodeIndexTracker.get(datum.id);
469     if (this.nodeBuffer[nodeIndex]) {
470       this.nodeBuffer[nodeIndex].x = xAndYCoords[0];
471       this.nodeBuffer[nodeIndex].y = xAndYCoords[1];
472       this.applyBufferDataToState();
473     }
474   }
475
476   addNodeInterpolator(nodeId, key, startingValue, endingValue, duration) {
477     let numberInterpolator = interpolateNumber(startingValue, endingValue);
478     let timeNow = now();
479     let interpolationObject = {
480       nodeId: nodeId, key: key, duration: duration, timeCreated: timeNow, method: numberInterpolator
481     };
482     this.interpolators.set(nodeId, interpolationObject);
483
484     if (!this.areInterpolationsRunning) {
485       this.interpolationTimer.restart(this.runInterpolators, simulationKeys.DATA_COPY_INTERVAL);
486       this.areInterpolationsRunning = true;
487     }
488   }
489
490   runInterpolators() {
491     // If we have no more interpolators to run then shut'r down!
492     if (this.interpolators.size === 0) {
493       this.interpolationTimer.stop();
494       this.areInterpolationsRunning = false;
495     }
496
497     let iterpolatorsComplete = [];
498     // Apply interpolation values
499     this.interpolators.forEach((interpolator) => {
500       let nodeIndex = this.nodeIndexTracker.get(interpolator.nodeId);
501       if (nodeIndex) {
502         let elapsedTime = now() - interpolator.timeCreated;
503         // Normalize t as D3's interpolateNumber needs a value between 0 and 1
504         let t = elapsedTime / interpolator.duration;
505         if (t >= 1) {
506           t = 1;
507           iterpolatorsComplete.push(interpolator.nodeId);
508         }
509         this.nodeBuffer[nodeIndex][interpolator.key] = interpolator.method(t);
510       }
511     });
512
513     // Remove any interpolators that are complete
514     if (iterpolatorsComplete.length > 0) {
515       for (let i = 0; i < iterpolatorsComplete.length; i++) {
516         this.interpolators.delete(iterpolatorsComplete[i]);
517       }
518     }
519
520     this.applyBufferDataToState();
521   }
522
523   render() {
524     // We will be using these values veru shortly, commenting out for eslint
525     // reasons so we can build for PV let {viewWidth, viewHeight} = this.props;
526     let {nodes, links, mainGroupTransform} = this.state;
527
528     return (
529       <div className='ts-force-selected-graph'>
530         <svg className={'fdgMainSvg'} width='100%' height='100%'>
531           <rect className={'fdgMainView'} x='0.5' y='0.5' width='99%'
532                 height='99%' fill='none'/>
533           <filter id='selected-node-drop-shadow'>
534             <feGaussianBlur in='SourceAlpha' stdDeviation='2.2'/>
535             <feOffset dx='-1' dy='1' result='offsetblur'/>
536             <feFlood floodColor='rgba(0,0,0,0.5)'/>
537             <feComposite in2='offsetblur' operator='in'/>
538             <feMerge>
539               <feMergeNode/>
540               <feMergeNode in='SourceGraphic'/>
541             </feMerge>
542           </filter>
543           <g className={'fdgMainG'} transform={mainGroupTransform}>
544             {links}
545             {nodes}
546           </g>
547         </svg>
548       </div>
549     );
550   }
551
552   static graphCounter = 0;
553
554   static generateNewProps(nodeArray, linkArray, metaData) {
555     ForceDirectedGraph.graphCounter += 1;
556     return {
557       graphCounter: ForceDirectedGraph.graphCounter,
558       nodeDataArray: nodeArray,
559       linkDataArray: linkArray,
560       graphMeta: metaData
561     };
562   }
563 }
564
565 export default ForceDirectedGraph;