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