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