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