1/** js sequence diagrams
2 *  https://bramp.github.io/js-sequence-diagrams/
3 *  (c) 2012-2017 Andrew Brampton (bramp.net)
4 *  Simplified BSD license.
5 */
6/*global Diagram, Snap, WebFont _ */
7// TODO Move defintion of font onto the <svg>, so it can easily be override at each level
8if (typeof Snap != 'undefined') {
9
10  var xmlns = 'http://www.w3.org/2000/svg';
11
12  var LINE = {
13    'stroke': '#000000',
14    'stroke-width': 2, // BUG TODO This gets set as a style, not as a attribute. Look at  eve.on("snap.util.attr"...
15    'fill': 'none'
16  };
17
18  var RECT = {
19        'stroke': '#000000',
20        'stroke-width': 2,
21        'fill': '#fff'
22      };
23
24  var LOADED_FONTS = {};
25
26  /******************
27   * SnapTheme
28   ******************/
29
30  var SnapTheme = function(diagram, options, resume) {
31        _.defaults(options, {
32            'css-class': 'simple',
33            'font-size': 16,
34            'font-family': 'Andale Mono, monospace'
35          });
36
37        this.init(diagram, options, resume);
38      };
39
40  _.extend(SnapTheme.prototype, BaseTheme.prototype, {
41
42    init: function(diagram, options, resume) {
43            BaseTheme.prototype.init.call(this, diagram);
44
45            this.paper_  = undefined;
46            this.cssClass_ = options['css-class'] || undefined;
47            this.font_ = {
48                'font-size': options['font-size'],
49                'font-family': options['font-family']
50              };
51
52            var a = this.arrowTypes_ = {};
53            a[ARROWTYPE.FILLED] = 'Block';
54            a[ARROWTYPE.OPEN]   = 'Open';
55
56            var l = this.lineTypes_ = {};
57            l[LINETYPE.SOLID]  = '';
58            l[LINETYPE.DOTTED] = '6,2';
59
60            var that = this;
61            this.waitForFont(function() {
62              resume(that);
63            });
64          },
65
66    // Wait for loading of the font
67    waitForFont: function(callback) {
68      var fontFamily = this.font_['font-family'];
69
70      if (typeof WebFont == 'undefined') {
71        throw new Error('WebFont is required (https://github.com/typekit/webfontloader).');
72      }
73
74      if (LOADED_FONTS[fontFamily]) {
75        // If already loaded, just return instantly.
76        callback();
77        return;
78      }
79
80      WebFont.load({
81          custom: {
82              families: [fontFamily] // TODO replace this with something that reads the css
83            },
84          classes: false, // No need to place classes on the DOM, just use JS Events
85          active: function() {
86              LOADED_FONTS[fontFamily] = true;
87              callback();
88            },
89          inactive: function() {
90              // If we fail to fetch the font, still continue.
91              LOADED_FONTS[fontFamily] = true;
92              callback();
93            }
94        });
95    },
96
97    addDescription: function(svg, description) {
98          var desc = document.createElementNS(xmlns, 'desc');
99          desc.appendChild(document.createTextNode(description));
100          svg.appendChild(desc);
101        },
102
103    setupPaper: function(container) {
104      // Container must be a SVG element. We assume it's a div, so lets create a SVG and insert
105      var svg = document.createElementNS(xmlns, 'svg');
106      container.appendChild(svg);
107
108      this.addDescription(svg, this.diagram.title || '');
109
110      this.paper_ = Snap(svg);
111      this.paper_.addClass('sequence');
112
113      if (this.cssClass_) {
114        this.paper_.addClass(this.cssClass_);
115      }
116
117      this.beginGroup();
118
119      // TODO Perhaps only include the markers if we actually use them.
120      var a = this.arrowMarkers_ = {};
121      var arrow = this.paper_.path('M 0 0 L 5 2.5 L 0 5 z');
122      a[ARROWTYPE.FILLED] = arrow.marker(0, 0, 5, 5, 5, 2.5)
123       .attr({id: 'markerArrowBlock'});
124
125      arrow = this.paper_.path('M 9.6,8 1.92,16 0,13.7 5.76,8 0,2.286 1.92,0 9.6,8 z');
126      a[ARROWTYPE.OPEN] = arrow.marker(0, 0, 9.6, 16, 9.6, 8)
127       .attr({markerWidth: '4', id: 'markerArrowOpen'});
128    },
129
130    layout: function() {
131      BaseTheme.prototype.layout.call(this);
132      this.paper_.attr({
133        width:  this.diagram.width + 'px',
134        height: this.diagram.height + 'px'
135      });
136    },
137
138    textBBox: function(text, font) {
139      // TODO getBBox will return the bounds with any whitespace/kerning. This makes some of our aligments screwed up
140      var t = this.createText(text, font);
141      var bb = t.getBBox();
142      t.remove();
143      return bb;
144    },
145
146    // For each drawn element, push onto the stack, so it can be wrapped in a single outer element
147    pushToStack: function(element) {
148      this._stack.push(element);
149      return element;
150    },
151
152    // Begin a group of elements
153    beginGroup: function() {
154      this._stack = [];
155    },
156
157    // Finishes the group, and returns the <group> element
158    finishGroup: function() {
159      var g = this.paper_.group.apply(this.paper_, this._stack);
160      this.beginGroup(); // Reset the group
161      return g;
162    },
163
164    createText: function(text, font) {
165      text = _.invoke(text.split('\n'), 'trim');
166      var t = this.paper_.text(0, 0, text);
167      t.attr(font || {});
168      if (text.length > 1) {
169        // Every row after the first, set tspan to be 1.2em below the previous line
170        t.selectAll('tspan:nth-child(n+2)').attr({
171          dy: '1.2em',
172          x: 0
173        });
174      }
175
176      return t;
177    },
178
179    drawLine: function(x1, y1, x2, y2, linetype, arrowhead) {
180      var line = this.paper_.line(x1, y1, x2, y2).attr(LINE);
181      if (linetype !== undefined) {
182        line.attr('strokeDasharray', this.lineTypes_[linetype]);
183      }
184      if (arrowhead !== undefined) {
185        line.attr('markerEnd', this.arrowMarkers_[arrowhead]);
186      }
187      return this.pushToStack(line);
188    },
189
190    drawRect: function(x, y, w, h) {
191      var rect = this.paper_.rect(x, y, w, h).attr(RECT);
192      return this.pushToStack(rect);
193    },
194
195    /**
196     * Draws text with a optional white background
197     * x,y (int) x,y top left point of the text, or the center of the text (depending on align param)
198     * text (string) text to print
199     * font (Object)
200     * align (string) ALIGN_LEFT or ALIGN_CENTER
201     */
202    drawText: function(x, y, text, font, align) {
203      var t = this.createText(text, font);
204      var bb = t.getBBox();
205
206      if (align == ALIGN_CENTER) {
207        x = x - bb.width / 2;
208        y = y - bb.height / 2;
209      }
210
211      // Now move the text into place
212      // `y - bb.y` because text(..) is positioned from the baseline, so this moves it down.
213      t.attr({x: x - bb.x, y: y - bb.y});
214      t.selectAll('tspan').attr({x: x});
215
216      this.pushToStack(t);
217      return t;
218    },
219
220    drawTitle: function() {
221      this.beginGroup();
222      BaseTheme.prototype.drawTitle.call(this);
223      return this.finishGroup().addClass('title');
224    },
225
226    drawActor: function(actor, offsetY, height) {
227      this.beginGroup();
228      BaseTheme.prototype.drawActor.call(this, actor, offsetY, height);
229      return this.finishGroup().addClass('actor');
230    },
231
232    drawSignal: function(signal, offsetY) {
233      this.beginGroup();
234      BaseTheme.prototype.drawSignal.call(this, signal, offsetY);
235      return this.finishGroup().addClass('signal');
236    },
237
238    drawSelfSignal: function(signal, offsetY) {
239      this.beginGroup();
240      BaseTheme.prototype.drawSelfSignal.call(this, signal, offsetY);
241      return this.finishGroup().addClass('signal');
242    },
243
244    drawNote: function(note, offsetY) {
245      this.beginGroup();
246      BaseTheme.prototype.drawNote.call(this, note, offsetY);
247      return this.finishGroup().addClass('note');
248    },
249  });
250
251  /******************
252   * SnapHandTheme
253   ******************/
254
255  var SnapHandTheme = function(diagram, options, resume) {
256        _.defaults(options, {
257            'css-class': 'hand',
258            'font-size': 16,
259            'font-family': 'danielbd'
260          });
261
262        this.init(diagram, options, resume);
263      };
264
265  // Take the standard SnapTheme and make all the lines wobbly
266  _.extend(SnapHandTheme.prototype, SnapTheme.prototype, {
267    drawLine: function(x1, y1, x2, y2, linetype, arrowhead) {
268      var line = this.paper_.path(handLine(x1, y1, x2, y2)).attr(LINE);
269      if (linetype !== undefined) {
270        line.attr('strokeDasharray', this.lineTypes_[linetype]);
271      }
272      if (arrowhead !== undefined) {
273        line.attr('markerEnd', this.arrowMarkers_[arrowhead]);
274      }
275      return this.pushToStack(line);
276    },
277
278    drawRect: function(x, y, w, h) {
279      var rect = this.paper_.path(handRect(x, y, w, h)).attr(RECT);
280      return this.pushToStack(rect);
281    }
282  });
283
284  registerTheme('snapSimple', SnapTheme);
285  registerTheme('snapHand',   SnapHandTheme);
286}
287