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