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