[SDC-29] Amdocs OnBoard 1707 initial commit.
[sdc.git] / dox-sequence-diagram-ui / src / main / webapp / lib / ecomp / asdc / sequencer / components / diagram / Diagram.jsx
1 /*!
2  * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
13  * or implied. See the License for the specific language governing
14  * permissions and limitations under the License.
15  */
16
17 import React from 'react';
18 import _template from 'lodash/template';
19 import _merge from 'lodash/merge';
20 import d3 from 'd3';
21
22 import Common from '../../common/Common';
23 import Logger from '../../common/Logger';
24 import Popup from './components/popup/Popup';
25
26 /**
27  * SVG diagram view.
28  */
29 export default class Diagram extends React.Component {
30
31   // ///////////////////////////////////////////////////////////////////////////////////////////////
32
33   /**
34    * Construct React view.
35    * @param props properties.
36    * @param context context.
37    */
38   constructor(props, context) {
39     super(props, context);
40
41     this.application = Common.assertNotNull(props.application);
42     this.options = this.application.getOptions().diagram;
43
44     this.events = {};
45     this.state = {
46       height: 0,
47       width: 0,
48     };
49
50     this.templates = {
51       diagram: _template(require('./templates/diagram.html')),
52       lifeline: _template(require('./templates/lifeline.html')),
53       message: _template(require('./templates/message.html')),
54       occurrence: _template(require('./templates/occurrence.html')),
55       fragment: _template(require('./templates/fragment.html')),
56       title: _template(require('./templates/title.html')),
57     };
58
59     this.handleResize = this.handleResize.bind(this);
60   }
61
62   // ///////////////////////////////////////////////////////////////////////////////////////////////
63
64   /**
65    * Set diagram name.
66    * @param n name.
67    */
68   setName(n) {
69     this.svg.select('').text(n);
70   }
71
72   // ///////////////////////////////////////////////////////////////////////////////////////////////
73
74   /**
75    * Get SVG from diagram.
76    * @returns {*|string}
77    */
78   getSVG() {
79     const svg = this.svg.node().outerHTML;
80     return svg.replace('<svg ', '<svg xmlns="http://www.w3.org/2000/svg" ');
81   }
82
83   // ///////////////////////////////////////////////////////////////////////////////////////////////
84
85   /**
86    * Select message by ID.
87    * @param id message ID.
88    */
89   selectMessage(id) {
90     const sel = this.svg.selectAll('g.asdcs-diagram-message-container');
91     sel.classed('asdcs-active', false);
92     sel.selectAll('rect.asdcs-diagram-message-bg').attr('filter', null);
93     if (id) {
94       const parent = this.svg.select(`g.asdcs-diagram-message-container[data-id="${id}"]`);
95       parent.classed('asdcs-active', true);
96       parent.selectAll('rect.asdcs-diagram-message-bg').attr('filter', 'url(#asdcsSvgHighlight)');
97     }
98     this._showNotesPopup(id);
99   }
100
101   // ///////////////////////////////////////////////////////////////////////////////////////////////
102
103   /**
104    * Select lifeline by ID.
105    * @param id lifeline ID.
106    */
107   selectLifeline(id) {
108     const sel = this.svg.selectAll('g.asdcs-diagram-lifeline-container');
109     sel.classed('asdcs-active', false);
110     sel.selectAll('rect').attr('filter', null);
111     if (id) {
112       const parent = this.svg.select(`g.asdcs-diagram-lifeline-container[data-id="${id}"]`);
113       parent.selectAll('rect').attr('filter', 'url(#asdcsSvgHighlight)');
114       parent.classed('asdcs-active', true);
115     }
116   }
117
118   // ///////////////////////////////////////////////////////////////////////////////////////////////
119
120   /**
121    * Handle resize, including initial sizing.
122    */
123   handleResize() {
124     if (this.wrapper) {
125       const height = this.wrapper.offsetHeight;
126       const width = this.wrapper.offsetWidth;
127       if (this.state.height !== height || this.state.width !== width) {
128         this.setState({ height, width });
129       }
130     }
131   }
132
133   // ///////////////////////////////////////////////////////////////////////////////////////////////
134
135   /**
136    * (Re)render diagram.
137    */
138   render() {
139
140     const model = this.application.getModel();
141     const modelJSON = model.unwrap();
142     const name = modelJSON.diagram.metadata.name;
143     const options = this.application.getOptions();
144     const titleHeight = options.diagram.title.height;
145     const titleClass = (titleHeight && titleHeight > 0) ? `height:${titleHeight}` : 'asdcs-hidden';
146
147     return (
148       <div className="asdcs-diagram">
149         <div className={`asdcs-diagram-name ${titleClass}`}>{name}</div>
150         <div className="asdcs-diagram-svg" ref={(r) => { this.wrapper = r; }}></div>
151         <Popup visible={false} ref={(r) => { this.popup = r; }} />
152       </div>
153     );
154   }
155
156   // ///////////////////////////////////////////////////////////////////////////////////////////////
157
158   redraw() {
159     this.updateSVG();
160   }
161
162   // ///////////////////////////////////////////////////////////////////////////////////////////////
163
164   /**
165    * Initial render.
166    */
167   componentDidMount() {
168     window.addEventListener('resize', this.handleResize);
169     this.updateSVG();
170
171     // Insurance:
172
173     setTimeout(() => {
174       this.handleResize();
175     }, 500);
176   }
177
178   // ///////////////////////////////////////////////////////////////////////////////////////////////
179
180   componentWillUnmount() {
181     window.removeEventListener('resize', this.handleResize);
182   }
183
184   // ///////////////////////////////////////////////////////////////////////////////////////////////
185
186   /**
187    * Render on update.
188    */
189   componentDidUpdate() {
190     this.updateSVG();
191   }
192
193   // ///////////////////////////////////////////////////////////////////////////////////////////////
194
195   /**
196    * Redraw SVG diagram. So far it's fast enough that it doesn't seem to matter whether
197    * it's completely redrawn.
198    */
199   updateSVG() {
200
201     if (!this.svg) {
202       const svgparams = _merge({}, this.options.svg);
203       this.wrapper.innerHTML = this.templates.diagram(svgparams);
204       this.svg = d3.select(this.wrapper).select('svg');
205     }
206
207     if (this.state.height === 0) {
208
209       // We'll get a resize event, and the height will be non-zero when it's actually time.
210
211       return;
212     }
213
214     if (this.state.height && this.state.width) {
215       const margin = this.options.svg.margin;
216       const x = -margin;
217       const y = -margin;
218       const height = this.state.height + (margin * 2);
219       const width = this.state.width + (margin * 2);
220       const viewBox = `${x} ${y} ${width} ${height}`;
221       this.svg.attr('viewBox', viewBox);
222     }
223
224
225     // If we've already rendered, then save the current scale/translate so that we
226     // can reapply it after rendering.
227
228     const gContentSelection = this.svg.selectAll('g.asdcs-diagram-content');
229     if (gContentSelection.size() === 1) {
230       const transform = gContentSelection.attr('transform');
231       if (transform) {
232         this.savedTransform = transform;
233       }
234     }
235
236     // Empty the document. We're starting again.
237
238     this.svg.selectAll('.asdcs-diagram-content').remove();
239
240     // Extract the model.
241
242     const model = this.application.getModel();
243     if (!model) {
244       return;
245     }
246     const modelJSON = model.unwrap();
247
248     // Extract dimension options.
249
250     const header = this.options.lifelines.header;
251     const spacing = this.options.lifelines.spacing;
252
253     // Make separate container elements so that we can control Z order.
254
255     const gContent = this.svg.append('g').attr('class', 'asdcs-diagram-content');
256     const gLifelines = gContent.append('g').attr('class', 'asdcs-diagram-lifelines');
257     const gCanvas = gContent.append('g').attr('class', 'asdcs-diagram-canvas');
258     gCanvas.append('g').attr('class', 'asdcs-diagram-occurrences');
259     gCanvas.append('g').attr('class', 'asdcs-diagram-fragments');
260     gCanvas.append('g').attr('class', 'asdcs-diagram-messages');
261
262     // Lifelines -----------------------------------------------------------------------------------
263
264     const actorsById = {};
265     const positionsByMessageId = {};
266     const lifelines = [];
267     for (const actor of modelJSON.diagram.lifelines) {
268       const x = (header.width / 2) + (lifelines.length * spacing.horizontal);
269       Diagram._processLifeline(actor, x);
270       lifelines.push({ x, actor });
271       actorsById[actor.id] = actor;
272     }
273
274     // Messages ------------------------------------------------------------------------------------
275
276     // Analyze occurrence information.
277
278     const occurrences = model.analyzeOccurrences();
279     const fragments = model.analyzeFragments();
280     let y = this.options.lifelines.header.height + spacing.vertical;
281     let messageIndex = 0;
282     for (const step of modelJSON.diagram.steps) {
283       if (step.message) {
284         positionsByMessageId[step.message.id] = positionsByMessageId[step.message.id] || {};
285         positionsByMessageId[step.message.id].y = y;
286         this._drawMessage(gCanvas, step.message, y, actorsById,
287           positionsByMessageId, ++messageIndex, occurrences, fragments);
288       }
289       y += spacing.vertical;
290     }
291
292     // ---------------------------------------------------------------------------------------------
293
294     // Draw the actual (dashed) lifelines in a background <g>.
295
296     this._drawLifelines(gLifelines, lifelines, y);
297
298     // Initialize mouse event handlers.
299
300     this._initMouseEvents(gLifelines, gCanvas);
301
302     // Scale to fit.
303
304     const bb = gContent.node().getBBox();
305     this._initZoom(gContent, bb.width, bb.height);
306   }
307
308   // ///////////////////////////////////////////////////////////////////////////////////////////////
309   // ///////////////////////////////////////////////////////////////////////////////////////////////
310   // ///////////////////////////////////////////////////////////////////////////////////////////////
311   // ///////////////////////////////////////////////////////////////////////////////////////////////
312   // ///////////////////////////////////////////////////////////////////////////////////////////////
313   // ///////////////////////////////////////////////////////////////////////////////////////////////
314   // ///////////////////////////////////////////////////////////////////////////////////////////////
315   // ///////////////////////////////////////////////////////////////////////////////////////////////
316   // ///////////////////////////////////////////////////////////////////////////////////////////////
317   // ///////////////////////////////////////////////////////////////////////////////////////////////
318
319   /**
320    * Draw message into SVG canvas.
321    * @param gCanvas container.
322    * @param message message to be rendered.
323    * @param y current y position.
324    * @param actorsById actor lookup.
325    * @param positionsByMessageId x- and y-position of each message.
326    * @param messageIndex where we are in the set of messages to be rendered.
327    * @param oData occurrences info.
328    * @param fData fragments info.
329    * @private
330    */
331   _drawMessage(gCanvas, message, y, actorsById, positionsByMessageId,
332                messageIndex, oData, fData) {
333
334     Common.assertNotNull(oData);
335
336     const request = message.type === 'request';
337     const fromActor = request ? actorsById[message.from] : actorsById[message.to];
338     const toActor = request ? actorsById[message.to] : actorsById[message.from];
339
340     if (!fromActor) {
341       Logger.warn(`Cannot draw message ${JSON.stringify(message)}: 'from' not found.`);
342       return;
343     }
344
345     if (!toActor) {
346       Logger.warn(`Cannot draw message ${JSON.stringify(message)}: 'to' not found.`);
347       return;
348     }
349
350     // Occurrences. --------------------------------------------------------------------------------
351
352     if (message.occurrence) {
353       Logger.debug(`Found occurrence for ${message.name}: ${JSON.stringify(message.occurrence)}`);
354     }
355     const activeTo = Diagram._calcActive(oData, toActor.id);
356     this._drawOccurrence(gCanvas, oData, positionsByMessageId, fromActor, message.id);
357     this._drawOccurrence(gCanvas, oData, positionsByMessageId, toActor, message.id);
358     const activeFrom = Diagram._calcActive(oData, fromActor.id);
359
360     // Messages. -----------------------------------------------------------------------------------
361
362     const gMessages = gCanvas.select('g.asdcs-diagram-messages');
363
364     // Save positions for later.
365
366     const positions = positionsByMessageId[message.id];
367     positions.x0 = fromActor.x;
368     positions.x1 = toActor.x;
369
370     // Calculate.
371
372     const leftToRight = fromActor.x < toActor.x;
373     const loopback = (message.to === message.from);
374     const x1 = this._calcMessageX(activeTo, toActor.x, true, leftToRight);
375     const x0 = loopback ? x1 : this._calcMessageX(activeFrom, fromActor.x, false, leftToRight);
376
377     let messagePath;
378     if (loopback) {
379
380       // To self.
381
382       messagePath = `M${x1},${y}`;
383       messagePath = `${messagePath} L${x1 + 200},${y}`;
384       messagePath = `${messagePath} L${x1 + 200},${y + 50}`;
385       messagePath = `${messagePath} L${x1},${y + 50}`;
386     } else {
387
388       // Between lifelines.
389
390       messagePath = `M${x0},${y}`;
391       messagePath = `${messagePath} L${x1},${y}`;
392     }
393
394     const styles = Diagram._getMessageStyles(message);
395
396     // Split message over lines.
397
398     const messageWithPrefix = `${messageIndex}. ${message.name}`;
399     const maxLines = this.options.messages.label.maxLines;
400     const wrapWords = this.options.messages.label.wrapWords;
401     const wrapLines = this.options.messages.label.wrapLines;
402     const messageLines = Common.tokenize(messageWithPrefix, wrapWords, wrapLines, maxLines);
403
404     const messageTxt = this.templates.message({
405       id: message.id,
406       classes: styles.css,
407       marker: styles.marker,
408       dasharray: styles.dasharray,
409       labels: messageLines,
410       lines: maxLines,
411       path: messagePath,
412       index: messageIndex,
413       x0, x1, y,
414     });
415
416     const messageEl = Common.txt2dom(messageTxt);
417     const gMessage = gMessages.append('g');
418     Common.dom2svg(messageEl, gMessage);
419
420     // Set the background's bounding box to that of the text,
421     // so that they fit snugly.
422
423     const labelBB = gMessage.select('.asdcs-diagram-message-label').node().getBBox();
424     gMessage.select('.asdcs-diagram-message-label-bg')
425       .attr('x', labelBB.x)
426       .attr('y', labelBB.y)
427       .attr('height', labelBB.height)
428       .attr('width', labelBB.width);
429
430     // Fragments. ----------------------------------------------------------------------------------
431
432     const fragment = fData[message.id];
433     if (fragment) {
434
435       // It ends on this message.
436
437       this._drawFragment(gCanvas, fragment, positionsByMessageId);
438
439     }
440   }
441
442   // ///////////////////////////////////////////////////////////////////////////////////////////////
443
444   /**
445    * Draw a single occurrence.
446    * @param gCanvas container.
447    * @param oData occurrence data.
448    * @param positionsByMessageId map of y positions by message ID.
449    * @param actor wrapper containing lifeline ID (.id), position (.x) and name (.name).
450    * @param messageId message identifier.
451    * @private
452    */
453   _drawOccurrence(gCanvas, oData, positionsByMessageId, actor, messageId) {
454
455     Common.assertType(oData, 'Object');
456     Common.assertType(positionsByMessageId, 'Object');
457     Common.assertType(actor, 'Object');
458     Common.assertType(messageId, 'String');
459
460     const gOccurrences = gCanvas.select('g.asdcs-diagram-occurrences');
461
462     const oOptions = this.options.lifelines.occurrences;
463     const oWidth = oOptions.width;
464     const oHalfWidth = oWidth / 2;
465     const oForeshortening = oOptions.foreshortening;
466     const oMarginTop = oOptions.marginTop;
467     const oMarginBottom = oOptions.marginBottom;
468     const o = oData[actor.id];
469
470     const active = Diagram._calcActive(oData, actor.id);
471
472     const x = (actor.x - oHalfWidth) + (active * oWidth);
473     const positions = positionsByMessageId[messageId];
474     const y = positions.y;
475
476     let draw = true;
477     if (o) {
478
479       if (o.start[messageId]) {
480
481         // Starting, but drawing nothing until we find the end.
482
483         o.active.push(messageId);
484         draw = false;
485
486       } else if (active > 0) {
487
488         const startMessageId = o.stop[messageId];
489         if (startMessageId) {
490
491           // OK, it ends here. Draw the occurrence box.
492
493           o.active.pop();
494           const foreshorteningY = active * oForeshortening;
495           const startY = positionsByMessageId[startMessageId].y;
496           const height = ((oMarginTop + oMarginBottom) + (y - startY)) - (foreshorteningY * 2);
497           const oProps = {
498             x: (actor.x - oHalfWidth) + ((active - 1) * oWidth),
499             y: ((startY - oMarginTop) + foreshorteningY),
500             height,
501             width: oWidth,
502           };
503
504           const occurrenceTxt = this.templates.occurrence(oProps);
505           const occurrenceEl = Common.txt2dom(occurrenceTxt);
506           Common.dom2svg(occurrenceEl, gOccurrences.append('g'));
507
508         }
509         draw = false;
510       }
511     }
512
513     if (draw) {
514
515       // Seems this is a singleton occurrence. We just draw a wee box around it.
516
517       const height = (oMarginTop + oMarginBottom);
518       const occurrenceProperties = { x, y: y - oMarginTop, height, width: oWidth };
519       const defaultTxt = this.templates.occurrence(occurrenceProperties);
520       const defaultEl = Common.txt2dom(defaultTxt);
521       Common.dom2svg(defaultEl, gOccurrences.append('g'));
522     }
523
524   }
525
526   // ///////////////////////////////////////////////////////////////////////////////////////////////
527
528   /**
529    * Draw box(es) around fragment(s).
530    * @param gCanvas container.
531    * @param fragment fragment definition, corresponding to its final (stop) message.
532    * @param positionsByMessageId message dimensions.
533    * @private
534    */
535   _drawFragment(gCanvas, fragment, positionsByMessageId) {
536
537     const optFragments = this.options.fragments;
538     const gFragments = gCanvas.select('g.asdcs-diagram-fragments');
539     const p1 = positionsByMessageId[fragment.stop];
540     if (p1 && fragment.start && fragment.start.length > 0) {
541
542       for (const start of fragment.start) {
543
544         const message = this.application.getModel().getMessageById(start);
545         const bounds = this._calcFragmentBounds(message, fragment, positionsByMessageId);
546         if (bounds) {
547
548           const maxLines = this.options.fragments.label.maxLines;
549           const wrapWords = this.options.fragments.label.wrapWords;
550           const wrapLines = this.options.fragments.label.wrapLines;
551           const lines = Common.tokenize(message.fragment.guard, wrapWords, wrapLines, maxLines);
552
553           const params = {
554             id: start,
555             x: bounds.x0 - optFragments.leftMargin,
556             y: bounds.y0 - optFragments.topMargin,
557             height: (bounds.y1 - bounds.y0) + optFragments.heightMargin,
558             width: (bounds.x1 - bounds.x0) + optFragments.widthMargin,
559             operator: (message.fragment.operator || 'alt'),
560             lines,
561           };
562
563           const fragmentTxt = this.templates.fragment(params);
564           const fragmentEl = Common.txt2dom(fragmentTxt);
565           const gFragment = gFragments.append('g');
566           Common.dom2svg(fragmentEl, gFragment);
567
568           const labelBB = gFragment.select('.asdcs-diagram-fragment-guard').node().getBBox();
569           gFragment.select('.asdcs-diagram-fragment-guard-bg')
570             .attr('x', labelBB.x)
571             .attr('y', labelBB.y)
572             .attr('height', labelBB.height)
573             .attr('width', labelBB.width);
574
575         } else {
576           Logger.warn(`Bad fragment: ${JSON.stringify(fragment)}`);
577         }
578       }
579     }
580   }
581
582   // ///////////////////////////////////////////////////////////////////////////////////////////////
583
584   _calcFragmentBounds(startMessage, fragment, positionsByMessageId) {
585     if (startMessage) {
586       const steps = this.application.getModel().unwrap().diagram.steps;
587       const bounds = { x0: 99999, x1: 0, y0: 99999, y1: 0 };
588       let foundStart = false;
589       let foundStop = false;
590       for (const step of steps) {
591         const message = step.message;
592         if (message) {
593           if (message.id === startMessage.id) {
594             foundStart = true;
595           }
596           if (foundStart && !foundStop) {
597             const positions = positionsByMessageId[message.id];
598             if (positions) {
599               bounds.x0 = Math.min(bounds.x0, Math.min(positions.x0, positions.x1));
600               bounds.y0 = Math.min(bounds.y0, positions.y);
601               bounds.x1 = Math.max(bounds.x1, Math.max(positions.x0, positions.x1));
602               bounds.y1 = Math.max(bounds.y1, positions.y);
603             } else {
604               // This probably means it hasn't been recorded yet, which is fine, because
605               // we draw fragments from where they END.
606               foundStop = true;
607             }
608           }
609
610           if (message.id === fragment.stop) {
611             foundStop = true;
612           }
613         }
614       }
615       return bounds;
616     }
617     return undefined;
618   }
619
620   // ///////////////////////////////////////////////////////////////////////////////////////////////
621
622   /**
623    * Draw all lifelines.
624    * @param gLifelines lifelines container.
625    * @param lifelines lifelines definitions.
626    * @param y height.
627    * @private
628    */
629   _drawLifelines(gLifelines, lifelines, y) {
630
631     const maxLines = this.options.lifelines.header.maxLines;
632     const wrapWords = this.options.lifelines.header.wrapWords;
633     const wrapLines = this.options.lifelines.header.wrapLines;
634
635     for (const lifeline of lifelines) {
636       const lines = Common.tokenize(lifeline.actor.name, wrapWords, wrapLines, maxLines);
637       const lifelineTxt = this.templates.lifeline({
638         x: lifeline.x,
639         y0: 0,
640         y1: y,
641         lines,
642         rows: maxLines,
643         headerHeight: this.options.lifelines.header.height,
644         headerWidth: this.options.lifelines.header.width,
645         id: lifeline.actor.id,
646       });
647
648       const lifelineEl = Common.txt2dom(lifelineTxt);
649       Common.dom2svg(lifelineEl, gLifelines.append('g'));
650     }
651   }
652
653   // ///////////////////////////////////////////////////////////////////////////////////////////////
654
655   /**
656    * Initialize all mouse events.
657    * @param gLifelines lifelines container.
658    * @param gCanvas top-level canvas container.
659    * @private
660    */
661   _initMouseEvents(gLifelines, gCanvas) {
662
663     const self = this;
664     const source = 'asdcs';
665     const origin = `${window.location.protocol}//${window.location.host}`;
666
667     let timer;
668     gLifelines.selectAll('.asdcs-diagram-lifeline-selectable')
669       .on('mouseenter', function f() {
670         timer = setTimeout(() => {
671           self.application.selectLifeline(d3.select(this.parentNode).attr('data-id'));
672         }, 150);
673       })
674       .on('mouseleave', () => {
675         clearTimeout(timer);
676         self.application.selectLifeline();
677       })
678       .on('click', function f() {
679         const id = d3.select(this.parentNode).attr('data-id');
680         window.postMessage({ source, id, type: 'lifeline' }, origin);
681       });
682
683     gLifelines.selectAll('.asdcs-diagram-lifeline-heading-box')
684       .on('mouseenter', function f() {
685         timer = setTimeout(() => {
686           self.application.selectLifeline(d3.select(this.parentNode).attr('data-id'));
687         }, 150);
688       })
689       .on('mouseleave', () => {
690         clearTimeout(timer);
691         self.application.selectLifeline();
692       })
693       .on('click', function f() {
694         const id = d3.select(this.parentNode).attr('data-id');
695         window.postMessage({ source, id, type: 'lifelineHeader' }, origin);
696       });
697
698     gCanvas.selectAll('.asdcs-diagram-message-selectable')
699       .on('mouseenter', function f() {
700         self.events.message = { x: d3.event.pageX, y: d3.event.pageY };
701         timer = setTimeout(() => {
702           self.application.selectMessage(d3.select(this.parentNode).attr('data-id'));
703         }, 200);
704       })
705       .on('mouseleave', () => {
706         delete self.events.message;
707         clearTimeout(timer);
708         self.application.selectMessage();
709       })
710       .on('click', function f() {
711         const id = d3.select(this.parentNode).attr('data-id');
712         window.postMessage({ source, id, type: 'message' }, origin);
713       });
714
715   }
716
717   // ///////////////////////////////////////////////////////////////////////////////////////////////
718
719   /**
720    * Get CSS classes to be applied to a message, according to whether request/response
721    * or synchronous/asynchronous.
722    * @param message message being rendered.
723    * @returns CSS class name(s).
724    * @private
725    */
726   static _getMessageStyles(message) {
727
728     let marker = 'asdcsDiagramArrowSolid';
729     let dasharray = '';
730     let css = 'asdcs-diagram-message';
731     if (message.type === 'request') {
732       css = `${css} asdcs-diagram-message-request`;
733     } else {
734       css = `${css} asdcs-diagram-message-response`;
735       marker = 'asdcsDiagramArrowOpen';
736       dasharray = '30, 10';
737     }
738
739     if (message.asynchronous) {
740       css = `${css} asdcs-diagram-message-asynchronous`;
741       marker = 'asdcsDiagramArrowOpen';
742     } else {
743       css = `${css} asdcs-diagram-message-synchronous`;
744     }
745
746     return { css, marker, dasharray };
747   }
748
749   // ///////////////////////////////////////////////////////////////////////////////////////////////
750
751   /**
752    * Initialize or reinitialize zoom. This sets the initial zoom in the case of
753    * a re-rendering, and initializes the eventhandling in all cases.
754    *
755    * It does some fairly risky parsing of the 'transform' attribute, assuming that it
756    * can contain scale() and translate(). But only the zoom handler and us are writing
757    * the transform values, so that's probably OK.
758    *
759    * @param gContent container.
760    * @param width diagram width.
761    * @param height diagram height.
762    * @private
763    */
764   _initZoom(gContent, width, height) {
765
766     const zoomed = function zoomed() {
767       gContent.attr('transform',
768         `translate(${d3.event.translate})scale(${d3.event.scale})`);
769     };
770
771     const viewWidth = this.state.width || this.options.svg.width;
772     const viewHeight = this.state.height || this.options.svg.height;
773     const scaleMinimum = this.options.svg.scale.minimum;
774     const scaleWidth = viewWidth / width;
775     const scaleHeight = viewHeight / height;
776
777     let scale = scaleMinimum;
778     if (this.options.svg.scale.width) {
779       scale = Math.max(scale, scaleWidth);
780     }
781     if (this.options.svg.scale.height) {
782       scale = Math.min(scale, scaleHeight);
783     }
784
785     scale = Math.max(scale, scaleMinimum);
786
787     let translate = [0, 0];
788     if (this.savedTransform) {
789       const s = this.savedTransform;
790       const scaleStart = s.indexOf('scale(');
791       if (scaleStart !== -1) {
792         scale = parseFloat(s.substring(scaleStart + 6, s.length - 1));
793       }
794       const translateStart = s.indexOf('translate(');
795       if (translateStart !== -1) {
796         const spec = s.substring(translateStart + 10, s.indexOf(')', translateStart));
797         const tokens = spec.split(',');
798         translate = [parseFloat(tokens[0]), parseFloat(tokens[1])];
799       }
800
801       gContent.attr('transform', this.savedTransform);
802     } else {
803       gContent.attr('transform', `scale(${scale})`);
804     }
805
806     const zoom = d3.behavior.zoom()
807       .scale(scale)
808       .scaleExtent([scaleMinimum, 10])
809       .translate(translate)
810       .on('zoom', zoomed);
811     this.svg.call(zoom);
812   }
813
814   // ///////////////////////////////////////////////////////////////////////////////////////////////
815
816   /**
817    * Hide from the linter the fact that we're modifying the lifeline.
818    * @param lifeline to be updated with X position.
819    * @param x X position.
820    * @private
821    */
822   static _processLifeline(lifeline, x) {
823     const actor = lifeline;
824     actor.id = actor.id || actor.name;
825     actor.x = x;
826   }
827
828   // ///////////////////////////////////////////////////////////////////////////////////////////////
829
830   /**
831    * Derive active occurrences for lifeline.
832    * @param oData occurrences data.
833    * @param lifelineId lifeline to be analyzed.
834    * @returns {number}
835    * @private
836    */
837   static _calcActive(oData, lifelineId) {
838     const o = oData[lifelineId];
839     let active = 0;
840     if (o && o.active) {
841       active = o.active.length;
842     }
843     return active;
844   }
845
846   // ///////////////////////////////////////////////////////////////////////////////////////////////
847
848   /**
849    * Derive the X position of an occurrence on a lifeline, taking into account how
850    * many occurrences are active.
851    * @param active active count.
852    * @param x lifeline X position; basis for offset.
853    * @param arrow whether this is the arrow (to) end.
854    * @param leftToRight whether this message goes left-to-right.
855    * @returns {*} calculated X position for occurrence left-hand side.
856    * @private
857    */
858   _calcMessageX(active, x, arrow, leftToRight) {
859     const width = this.options.lifelines.occurrences.width;
860     const halfWidth = width / 2;
861     const active0 = Math.max(0, active - 1);
862     let calculated = x + (active0 * width);
863     if (arrow) {
864       // End (ARROW).
865       if (leftToRight) {
866         calculated -= halfWidth;
867       } else {
868         calculated += halfWidth;
869       }
870     } else {
871       // Start (NOT ARROW).
872       if (leftToRight) {
873         calculated += halfWidth;
874       } else {
875         calculated -= halfWidth;
876       }
877     }
878
879     return calculated;
880   }
881
882   // ///////////////////////////////////////////////////////////////////////////////////////////////
883
884   /**
885    * Show popup upon hovering over a messages that has associated notes.
886    * @param id
887    * @private
888    */
889   _showNotesPopup(id) {
890     if (this.popup) {
891       if (id) {
892         const message = this.application.getModel().getMessageById(id);
893         if (message && message.notes && message.notes.length > 0 && this.events.message) {
894           this.popup.setState({
895             visible: true,
896             left: this.events.message.x - 50,
897             top: this.events.message.y + 20,
898             notes: message.notes[0],
899           });
900         }
901       } else {
902         this.popup.setState({ visible: false, notes: '' });
903       }
904     }
905   }
906 }
907
908
909 Diagram.propTypes = {
910   application: React.PropTypes.object.isRequired,
911 };