blob: dfe22411bff0d5164495efa1ce19d271e7328c94 [file] [log] [blame]
Akronf5dc5102017-05-16 20:32:57 +02001define(function () {
2 "use strict";
3
4 var svgNS = "http://www.w3.org/2000/svg";
5
6 return {
7 create : function (snippet) {
8 var obj = Object.create(this)._init(snippet);
Akronc5b5f742017-05-23 16:04:35 +02009 obj._tokens = [];
Akronc5b5f742017-05-23 16:04:35 +020010 obj._arcs = []
Akron3a4a08e2017-05-23 22:34:18 +020011 obj._tokenElements = [];
12 obj._y = 0;
Akrond67d45b2017-05-18 21:47:38 +020013
Akronc5b5f742017-05-23 16:04:35 +020014 // Some configurations
Akronf5dc5102017-05-16 20:32:57 +020015 obj.maxArc = 200; // maximum height of the bezier control point
Akron65d31082017-09-08 16:23:40 +020016 obj.overlapDiff = 40;
Akronc5b5f742017-05-23 16:04:35 +020017 obj.arcDiff = 15;
Akron65d31082017-09-08 16:23:40 +020018 obj.anchorDiff = 8;
Akron3a4a08e2017-05-23 22:34:18 +020019 obj.anchorStart = 15;
Akronc5b5f742017-05-23 16:04:35 +020020 obj.tokenSep = 30;
Akron65d31082017-09-08 16:23:40 +020021 obj.xPadding = 10;
22 obj.yPadding = 5;
Akronf5dc5102017-05-16 20:32:57 +020023 return obj;
24 },
25
26 _init : function (snippet) {
27 /*
28 var html = document.createElement("div");
29 html.innerHTML = snippet;
30 */
31 return this;
32 },
33
Akron15175132017-09-07 18:12:55 +020034
Akronf5dc5102017-05-16 20:32:57 +020035 // This is a shorthand for SVG element creation
36 _c : function (tag) {
37 return document.createElementNS(svgNS, tag);
38 },
39
Akron15175132017-09-07 18:12:55 +020040
Akronf5dc5102017-05-16 20:32:57 +020041 // Returns the center point of the requesting token
42 _tokenPoint : function (node) {
43 var box = node.getBoundingClientRect();
44 return box.x + (box.width / 2);
45 },
46
Akron15175132017-09-07 18:12:55 +020047
48 // Draws an anchor
Akrond67d45b2017-05-18 21:47:38 +020049 _drawAnchor : function (anchor) {
Akron15175132017-09-07 18:12:55 +020050
51 // Calculate the span of the first and last token, the anchor spans
Akron3a4a08e2017-05-23 22:34:18 +020052 var firstBox = this._tokenElements[anchor.first].getBoundingClientRect();
53 var lastBox = this._tokenElements[anchor.last].getBoundingClientRect();
Akrond67d45b2017-05-18 21:47:38 +020054
Akron15175132017-09-07 18:12:55 +020055 var startPos = firstBox.left - this.offsetLeft;
56 var endPos = lastBox.right - this.offsetLeft;
57
Akron3a4a08e2017-05-23 22:34:18 +020058 var y = this._y + (anchor.overlaps * this.anchorDiff) - this.anchorStart;
59
Akrond67d45b2017-05-18 21:47:38 +020060 var l = this._c('path');
Akron65d31082017-09-08 16:23:40 +020061 this._arcsElement.appendChild(l);
Akron3a4a08e2017-05-23 22:34:18 +020062 l.setAttribute("d", "M " + startPos + "," + y + " L " + endPos + "," + y);
Akrond67d45b2017-05-18 21:47:38 +020063 l.setAttribute("class", "anchor");
64 anchor.element = l;
65 anchor.y = y;
66 return l;
67 },
68
Akronf5dc5102017-05-16 20:32:57 +020069 // Create an arc with a label
70 // Potentially needs a height parameter for stacks
Akron63ae00b2017-05-16 22:03:36 +020071 _drawArc : function (arc) {
Akronf5dc5102017-05-16 20:32:57 +020072
Akrond67d45b2017-05-18 21:47:38 +020073 var startPos, endPos;
Akron3a4a08e2017-05-23 22:34:18 +020074 var startY = this._y, endY = this._y;
Akronf5dc5102017-05-16 20:32:57 +020075
Akrond67d45b2017-05-18 21:47:38 +020076 if (arc.startAnchor !== undefined) {
Akron3a4a08e2017-05-23 22:34:18 +020077 startPos = this._tokenPoint(arc.startAnchor.element);
Akrond67d45b2017-05-18 21:47:38 +020078 startY = arc.startAnchor.y;
79 }
80 else {
81 startPos = this._tokenPoint(this._tokenElements[arc.first]);
82 };
83
84 if (arc.endAnchor !== undefined) {
85 endPos = this._tokenPoint(arc.endAnchor.element)
86 endY = arc.endAnchor.y;
87 }
88 else {
89 endPos = this._tokenPoint(this._tokenElements[arc.last]);
90 };
91
Akron15175132017-09-07 18:12:55 +020092 startPos -= this.offsetLeft;
93 endPos -= this.offsetLeft;
94
Akronf5dc5102017-05-16 20:32:57 +020095 var g = this._c("g");
Akron65d31082017-09-08 16:23:40 +020096 g.setAttribute("class", "arc");
Akronf5dc5102017-05-16 20:32:57 +020097 var p = g.appendChild(this._c("path"));
Akron15175132017-09-07 18:12:55 +020098 p.setAttribute('class', 'edge');
Akron65d31082017-09-08 16:23:40 +020099
100 // Attach the new arc before drawing, so computed values are available
101 this._arcsElement.appendChild(g);
Akronf5dc5102017-05-16 20:32:57 +0200102
103 // Create arc
104 var middle = Math.abs(endPos - startPos) / 2;
105
Akron63ae00b2017-05-16 22:03:36 +0200106 // TODO: take the number of tokens into account!
Akronc5b5f742017-05-23 16:04:35 +0200107 var cHeight = this.arcDiff + arc.overlaps * this.overlapDiff + (middle / 2);
108
109 // Respect the maximum height
110 cHeight = cHeight < this.maxArc ? cHeight : this.maxArc;
Akronf5dc5102017-05-16 20:32:57 +0200111
112 var x = Math.min(startPos, endPos);
Akronc5b5f742017-05-23 16:04:35 +0200113
Akron3a4a08e2017-05-23 22:34:18 +0200114 //var controlY = (startY + endY - cHeight);
115 var controlY = (endY - cHeight);
Akronf5dc5102017-05-16 20:32:57 +0200116
Akron3a4a08e2017-05-23 22:34:18 +0200117 var arcE = "M "+ startPos + "," + startY +
118 " C " + startPos + "," + controlY +
119 " " + endPos + "," + controlY +
120 " " + endPos + "," + endY;
Akrond67d45b2017-05-18 21:47:38 +0200121
Akron63ae00b2017-05-16 22:03:36 +0200122 p.setAttribute("d", arcE);
Akronf5dc5102017-05-16 20:32:57 +0200123
Akron3a4a08e2017-05-23 22:34:18 +0200124 if (arc.direction !== undefined) {
125 p.setAttribute("marker-end", "url(#arr)");
126 if (arc.direction === 'bi') {
127 p.setAttribute("marker-start", "url(#arr)");
128 };
129 };
130
Akronc5b5f742017-05-23 16:04:35 +0200131 /*
132 * Calculate the top point of the arc for labeling using
133 * de Casteljau's algorithm, see e.g.
134 * http://blog.sklambert.com/finding-the-control-points-of-a-bezier-curve/
135 * of course simplified to symmetric arcs ...
136 */
137 // Interpolate one side of the control polygon
138 // var controlInterpY1 = (startY + controlY) / 2;
139 // var controlInterpY2 = (controlInterpY1 + controlY) / 2;
140 var middleY = (((startY + controlY) / 2) + controlY) / 2;
141
142 // WARNING!
143 // This won't respect span anchors, adjusting startY and endY!
144
Akron63ae00b2017-05-16 22:03:36 +0200145 if (arc.label !== undefined) {
Akronf5dc5102017-05-16 20:32:57 +0200146 var labelE = g.appendChild(this._c("text"));
147 labelE.setAttribute("x", x + middle);
Akronc5b5f742017-05-23 16:04:35 +0200148 labelE.setAttribute("y", middleY + 3);
Akronf5dc5102017-05-16 20:32:57 +0200149 labelE.setAttribute("text-anchor", "middle");
Akron65d31082017-09-08 16:23:40 +0200150 var textNode = document.createTextNode(arc.label);
151 labelE.appendChild(textNode);
Akron1dc87902017-05-29 16:04:56 +0200152
Akron65d31082017-09-08 16:23:40 +0200153 var labelBox = labelE.getBBox();
154 var textWidth = labelBox.width; // labelE.getComputedTextLength();
155 var textHeight = labelBox.height; // labelE.getComputedTextLength();
Akron1dc87902017-05-29 16:04:56 +0200156
Akron65d31082017-09-08 16:23:40 +0200157 // Add padding to left and right
158
159 // var labelR = g.appendChild(this._c("rect"));
160 var labelR = g.insertBefore(this._c("rect"), labelE);
161 var boxWidth = textWidth + 2 * this.xPadding;
162 labelR.setAttribute("x", x + middle - (boxWidth / 2));
163 labelR.setAttribute("ry", 5);
164 labelR.setAttribute("y", labelBox.y - this.yPadding);
165 labelR.setAttribute("width", boxWidth);
166 labelR.setAttribute("height", textHeight + 2*this.yPadding);
Akronf5dc5102017-05-16 20:32:57 +0200167 };
Akron65d31082017-09-08 16:23:40 +0200168
169 // return g;
Akronf5dc5102017-05-16 20:32:57 +0200170 },
171
172 element : function () {
173 if (this._element !== undefined)
174 return this._element;
175
176 // Create svg
177 var svg = this._c("svg");
Akron3a4a08e2017-05-23 22:34:18 +0200178
179 window.addEventListener("resize", function () {
180 // TODO: Only if text-size changed!
181 this.show();
182 }.bind(this));
183
184 var defs = svg.appendChild(this._c("defs"));
185 var marker = defs.appendChild(this._c("marker"));
186 marker.setAttribute("refX", 9);
187 marker.setAttribute("id", "arr");
188 marker.setAttribute("orient", "auto-start-reverse");
189 marker.setAttribute("markerUnits","userSpaceOnUse");
190
191 var arrow = this._c("path");
192 arrow.setAttribute("transform", "scale(0.8)");
193 arrow.setAttribute("d", "M 0,-5 0,5 10,0 Z");
194 marker.appendChild(arrow);
195
Akronf5dc5102017-05-16 20:32:57 +0200196 this._element = svg;
197 return this._element;
198 },
199
200 // Add a relation with a start, an end,
201 // a direction value and a label text
Akronc5b5f742017-05-23 16:04:35 +0200202 addRel : function (rel) {
203 this._arcs.push(rel);
204 return this;
Akronf5dc5102017-05-16 20:32:57 +0200205 },
206
Akronc5b5f742017-05-23 16:04:35 +0200207
208 addToken : function(token) {
209 this._tokens.push(token);
210 return this;
211 },
212
Akronf5dc5102017-05-16 20:32:57 +0200213 /*
214 * All arcs need to be sorted before shown,
215 * to avoid nesting.
216 */
217 _sortArcs : function () {
218
Akrond67d45b2017-05-18 21:47:38 +0200219
220 // TODO:
221 // Keep in mind that the arcs may have long anchors!
222 // 1. Iterate over all arcs
223 // 2. Sort all multi
224 var anchors = [];
225
Akronf5dc5102017-05-16 20:32:57 +0200226 // 1. Sort by length
227 // 2. Tag all spans with the number of overlaps before
228 // a) Iterate over all spans
229 // b) check the latest preceeding overlapping span (lpos)
230 // -> not found: tag with 0
231 // -> found: Add +1 to the level of the (lpos)
232 // c) If the new tag is smaller than the previous element,
233 // reorder
Akron63ae00b2017-05-16 22:03:36 +0200234
235 // Normalize start and end
236 var sortedArcs = this._arcs.map(function (v) {
Akrond67d45b2017-05-18 21:47:38 +0200237
238 // Check for long anchors
239 if (v.start instanceof Array) {
240 var middle = Math.ceil(Math.abs(v.start[1] - v.start[0]) / 2) + v.start[0];
241
242 v.startAnchor = {
Akron3a4a08e2017-05-23 22:34:18 +0200243 "first": v.start[0],
244 "last" : v.start[1],
Akrond67d45b2017-05-18 21:47:38 +0200245 "length" : v.start[1] - v.start[0]
246 };
247
248 // Add to anchors list
249 anchors.push(v.startAnchor);
250 v.start = middle;
251 };
252
253 if (v.end instanceof Array) {
254 var middle = Math.abs(v.end[0] - v.end[1]) + v.end[0];
255 v.endAnchor = {
Akron3a4a08e2017-05-23 22:34:18 +0200256 "first": v.end[0],
257 "last" : v.end[1],
Akrond67d45b2017-05-18 21:47:38 +0200258 "length" : v.end[1] - v.end[0]
259 };
260
261 // Add to anchors list
262 anchors.push(v.endAnchor);
263 v.end = middle;
264 };
265
266 // calculate the arch length
Akron63ae00b2017-05-16 22:03:36 +0200267 if (v.start < v.end) {
268 v.first = v.start;
269 v.last = v.end;
270 v.length = v.end - v.start;
271 }
272 else {
273 v.first = v.end;
274 v.last = v.start;
275 v.length = v.start - v.end;
276 };
277 return v;
278 });
279
280 // Sort based on length
281 sortedArcs.sort(function (a, b) {
282 if (a.length < b.length)
283 return -1;
284 else
285 return 1;
286 });
287
Akron3a4a08e2017-05-23 22:34:18 +0200288 this._sortedArcs = lengthSort(sortedArcs, false);
Akrond67d45b2017-05-18 21:47:38 +0200289 this._sortedAnchors = lengthSort(anchors, true);
Akronf5dc5102017-05-16 20:32:57 +0200290 },
291
292 show : function () {
293 var svg = this._element;
Akron3a4a08e2017-05-23 22:34:18 +0200294 var height = this.maxArc;
295
Akron3a4a08e2017-05-23 22:34:18 +0200296 // Delete old group
297 if (svg.getElementsByTagName("g")[0] !== undefined) {
298 var group = svg.getElementsByTagName("g")[0];
299 svg.removeChild(group);
300 this._tokenElements = [];
301 };
302
303 var g = svg.appendChild(this._c("g"));
Akronf5dc5102017-05-16 20:32:57 +0200304
305 /*
Akron15175132017-09-07 18:12:55 +0200306 * Create token list
Akronf5dc5102017-05-16 20:32:57 +0200307 */
Akron3a4a08e2017-05-23 22:34:18 +0200308 var text = g.appendChild(this._c("text"));
Akron3d204282017-09-07 18:24:18 +0200309 text.setAttribute('class', 'leaf');
Akron3a4a08e2017-05-23 22:34:18 +0200310 text.setAttribute("text-anchor", "start");
311 text.setAttribute("y", height);
Akronf5dc5102017-05-16 20:32:57 +0200312
Akron3a4a08e2017-05-23 22:34:18 +0200313 this._y = height - (this.anchorStart);
314
315 var ws = text.appendChild(this._c("tspan"));
316 ws.appendChild(document.createTextNode('\u00A0'));
317 ws.style.textAnchor = "start";
318
Akronf5dc5102017-05-16 20:32:57 +0200319 var lastRight = 0;
320 for (var node_i in this._tokens) {
321 // Append svg
322 var tspan = text.appendChild(this._c("tspan"));
323 tspan.appendChild(document.createTextNode(this._tokens[node_i]));
Akron3a4a08e2017-05-23 22:34:18 +0200324 tspan.setAttribute("text-anchor", "middle");
325
Akronf5dc5102017-05-16 20:32:57 +0200326 this._tokenElements.push(tspan);
327
328 // Add whitespace!
Akron3a4a08e2017-05-23 22:34:18 +0200329 //var ws = text.appendChild(this._c("tspan"));
330 //ws.appendChild(document.createTextNode(" "));
331 // ws.setAttribute("class", "rel-ws");
332 tspan.setAttribute("dx", this.tokenSep);
Akronf5dc5102017-05-16 20:32:57 +0200333 };
334
Akron15175132017-09-07 18:12:55 +0200335 var globalBoundingBox = g.getBoundingClientRect();
336 this.offsetLeft = globalBoundingBox.left;
337
Akron3a4a08e2017-05-23 22:34:18 +0200338 var arcs = g.appendChild(this._c("g"));
Akron65d31082017-09-08 16:23:40 +0200339 this._arcsElement = arcs;
340
Akron3a4a08e2017-05-23 22:34:18 +0200341 arcs.classList.add("arcs");
Akron15175132017-09-07 18:12:55 +0200342
343 // Sort arcs if not sorted yet
Akron3a4a08e2017-05-23 22:34:18 +0200344 if (this._sortedArcs === undefined) {
345 this._sortArcs();
346 };
Akrond67d45b2017-05-18 21:47:38 +0200347
348 var i;
Akron15175132017-09-07 18:12:55 +0200349
350 // Draw all anchors
Akrond67d45b2017-05-18 21:47:38 +0200351 for (i in this._sortedAnchors) {
Akron65d31082017-09-08 16:23:40 +0200352 this._drawAnchor(this._sortedAnchors[i]);
Akrond67d45b2017-05-18 21:47:38 +0200353 };
Akron15175132017-09-07 18:12:55 +0200354
355
356 // draw all arcs
Akrond67d45b2017-05-18 21:47:38 +0200357 for (i in this._sortedArcs) {
Akron65d31082017-09-08 16:23:40 +0200358 this._drawArc(this._sortedArcs[i]);
Akronf5dc5102017-05-16 20:32:57 +0200359 };
Akron3a4a08e2017-05-23 22:34:18 +0200360
361 var width = text.getBoundingClientRect().width;
Akronf3d7d8e2017-05-23 22:52:54 +0200362 svg.setAttribute("width", width + 20);
363 svg.setAttribute("height", height + 20);
Akron3a4a08e2017-05-23 22:34:18 +0200364 svg.setAttribute("class", "relTree");
Akronf5dc5102017-05-16 20:32:57 +0200365 }
Akrond67d45b2017-05-18 21:47:38 +0200366 };
367
368 function lengthSort (list, inclusive) {
369
370 /*
371 * The "inclusive" flag allows to
372 * modify the behaviour for inclusivity check,
373 * e.g. if identical start or endpoints mean overlap or not.
374 */
375
376 var stack = [];
377
378 // Iterate over all definitions
379 for (var i = 0; i < list.length; i++) {
380 var current = list[i];
381
382 // Check the stack order
383 var overlaps = 0;
384
385 for (var j = (stack.length - 1); j >= 0; j--) {
386 var check = stack[j];
387
388 // (a..(b..b)..a)
389 if (current.first <= check.first && current.last >= check.last) {
390 overlaps = check.overlaps + 1;
391 break;
392 }
393
394 // (a..(b..a)..b)
395 else if (current.first <= check.first && current.last >= check.first) {
396
397 if (inclusive || (current.first != check.first && current.last != check.first)) {
398 overlaps = check.overlaps + (current.length == check.length ? 0 : 1);
399 };
400 }
401
402 // (b..(a..b)..a)
403 else if (current.first <= check.last && current.last >= check.last) {
404
405 if (inclusive || (current.first != check.last && current.last != check.last)) {
406 overlaps = check.overlaps + (current.length == check.length ? 0 : 1);
407 };
408 };
409 };
410
411 // Set overlaps
412 current.overlaps = overlaps;
413
414 stack.push(current);
415
416 // Although it is already sorted,
417 // the new item has to be put at the correct place
418 // TODO: Use something like splice() instead
419 stack.sort(function (a,b) {
420 b.overlaps - a.overlaps
421 });
422 };
423
424 return stack;
425 };
Akronf5dc5102017-05-16 20:32:57 +0200426});