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