blob: f79d1899d8f880dc4ebc9b0079d30e6c42de62c9 [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
Akronc5b5f742017-05-23 16:04:35 +020016 obj.overlapDiff = 20;
17 obj.arcDiff = 15;
18 obj.anchorDiff = 6;
Akron3a4a08e2017-05-23 22:34:18 +020019 obj.anchorStart = 15;
Akronc5b5f742017-05-23 16:04:35 +020020 obj.tokenSep = 30;
Akronf5dc5102017-05-16 20:32:57 +020021 return obj;
22 },
23
24 _init : function (snippet) {
25 /*
26 var html = document.createElement("div");
27 html.innerHTML = snippet;
28 */
29 return this;
30 },
31
32 // This is a shorthand for SVG element creation
33 _c : function (tag) {
34 return document.createElementNS(svgNS, tag);
35 },
36
37 // Returns the center point of the requesting token
38 _tokenPoint : function (node) {
39 var box = node.getBoundingClientRect();
40 return box.x + (box.width / 2);
41 },
42
Akrond67d45b2017-05-18 21:47:38 +020043 _drawAnchor : function (anchor) {
Akron3a4a08e2017-05-23 22:34:18 +020044 var firstBox = this._tokenElements[anchor.first].getBoundingClientRect();
45 var lastBox = this._tokenElements[anchor.last].getBoundingClientRect();
Akrond67d45b2017-05-18 21:47:38 +020046
Akron3a4a08e2017-05-23 22:34:18 +020047 var startPos = firstBox.left;
48 var endPos = lastBox.right;
49
50 var y = this._y + (anchor.overlaps * this.anchorDiff) - this.anchorStart;
51
Akrond67d45b2017-05-18 21:47:38 +020052 var l = this._c('path');
Akron3a4a08e2017-05-23 22:34:18 +020053 l.setAttribute("d", "M " + startPos + "," + y + " L " + endPos + "," + y);
Akrond67d45b2017-05-18 21:47:38 +020054 l.setAttribute("class", "anchor");
55 anchor.element = l;
56 anchor.y = y;
57 return l;
58 },
59
Akronf5dc5102017-05-16 20:32:57 +020060 // Create an arc with a label
61 // Potentially needs a height parameter for stacks
Akron63ae00b2017-05-16 22:03:36 +020062 _drawArc : function (arc) {
Akronf5dc5102017-05-16 20:32:57 +020063
Akrond67d45b2017-05-18 21:47:38 +020064 var startPos, endPos;
Akron3a4a08e2017-05-23 22:34:18 +020065 var startY = this._y, endY = this._y;
Akronf5dc5102017-05-16 20:32:57 +020066
Akrond67d45b2017-05-18 21:47:38 +020067 if (arc.startAnchor !== undefined) {
Akron3a4a08e2017-05-23 22:34:18 +020068 startPos = this._tokenPoint(arc.startAnchor.element);
Akrond67d45b2017-05-18 21:47:38 +020069 startY = arc.startAnchor.y;
70 }
71 else {
72 startPos = this._tokenPoint(this._tokenElements[arc.first]);
73 };
74
75 if (arc.endAnchor !== undefined) {
76 endPos = this._tokenPoint(arc.endAnchor.element)
77 endY = arc.endAnchor.y;
78 }
79 else {
80 endPos = this._tokenPoint(this._tokenElements[arc.last]);
81 };
82
Akronf5dc5102017-05-16 20:32:57 +020083 var g = this._c("g");
84 var p = g.appendChild(this._c("path"));
85
86 // Create arc
87 var middle = Math.abs(endPos - startPos) / 2;
88
Akron63ae00b2017-05-16 22:03:36 +020089 // TODO: take the number of tokens into account!
Akronc5b5f742017-05-23 16:04:35 +020090 var cHeight = this.arcDiff + arc.overlaps * this.overlapDiff + (middle / 2);
91
92 // Respect the maximum height
93 cHeight = cHeight < this.maxArc ? cHeight : this.maxArc;
Akronf5dc5102017-05-16 20:32:57 +020094
95 var x = Math.min(startPos, endPos);
Akronc5b5f742017-05-23 16:04:35 +020096
Akron3a4a08e2017-05-23 22:34:18 +020097 //var controlY = (startY + endY - cHeight);
98 var controlY = (endY - cHeight);
Akronf5dc5102017-05-16 20:32:57 +020099
Akron3a4a08e2017-05-23 22:34:18 +0200100 var arcE = "M "+ startPos + "," + startY +
101 " C " + startPos + "," + controlY +
102 " " + endPos + "," + controlY +
103 " " + endPos + "," + endY;
Akrond67d45b2017-05-18 21:47:38 +0200104
Akron63ae00b2017-05-16 22:03:36 +0200105 p.setAttribute("d", arcE);
Akronf5dc5102017-05-16 20:32:57 +0200106
Akron3a4a08e2017-05-23 22:34:18 +0200107 if (arc.direction !== undefined) {
108 p.setAttribute("marker-end", "url(#arr)");
109 if (arc.direction === 'bi') {
110 p.setAttribute("marker-start", "url(#arr)");
111 };
112 };
113
Akronc5b5f742017-05-23 16:04:35 +0200114 /*
115 * Calculate the top point of the arc for labeling using
116 * de Casteljau's algorithm, see e.g.
117 * http://blog.sklambert.com/finding-the-control-points-of-a-bezier-curve/
118 * of course simplified to symmetric arcs ...
119 */
120 // Interpolate one side of the control polygon
121 // var controlInterpY1 = (startY + controlY) / 2;
122 // var controlInterpY2 = (controlInterpY1 + controlY) / 2;
123 var middleY = (((startY + controlY) / 2) + controlY) / 2;
124
125 // WARNING!
126 // This won't respect span anchors, adjusting startY and endY!
127
Akron63ae00b2017-05-16 22:03:36 +0200128 if (arc.label !== undefined) {
Akronf5dc5102017-05-16 20:32:57 +0200129 var labelE = g.appendChild(this._c("text"));
130 labelE.setAttribute("x", x + middle);
Akronc5b5f742017-05-23 16:04:35 +0200131 labelE.setAttribute("y", middleY + 3);
Akronf5dc5102017-05-16 20:32:57 +0200132 labelE.setAttribute("text-anchor", "middle");
Akron63ae00b2017-05-16 22:03:36 +0200133 labelE.appendChild(document.createTextNode(arc.label));
Akron1dc87902017-05-29 16:04:56 +0200134
135 /*
136 var textWidth = labelE.getComputedTextLength();
137
138 var labelR = g.appendChild(this._c("rect"));
139 labelR.setAttribute("x", x + middle);
140 labelR.setAttribute("y", middleY + 3);
141 labelR.setAttribute("width", textWidth +30);
142 labelR.setAttribute("height", 20);
143 */
Akronf5dc5102017-05-16 20:32:57 +0200144 };
Akronf5dc5102017-05-16 20:32:57 +0200145 return g;
146 },
147
148 element : function () {
149 if (this._element !== undefined)
150 return this._element;
151
152 // Create svg
153 var svg = this._c("svg");
Akron3a4a08e2017-05-23 22:34:18 +0200154
155 window.addEventListener("resize", function () {
156 // TODO: Only if text-size changed!
157 this.show();
158 }.bind(this));
159
160 var defs = svg.appendChild(this._c("defs"));
161 var marker = defs.appendChild(this._c("marker"));
162 marker.setAttribute("refX", 9);
163 marker.setAttribute("id", "arr");
164 marker.setAttribute("orient", "auto-start-reverse");
165 marker.setAttribute("markerUnits","userSpaceOnUse");
166
167 var arrow = this._c("path");
168 arrow.setAttribute("transform", "scale(0.8)");
169 arrow.setAttribute("d", "M 0,-5 0,5 10,0 Z");
170 marker.appendChild(arrow);
171
Akronf5dc5102017-05-16 20:32:57 +0200172 this._element = svg;
173 return this._element;
174 },
175
176 // Add a relation with a start, an end,
177 // a direction value and a label text
Akronc5b5f742017-05-23 16:04:35 +0200178 addRel : function (rel) {
179 this._arcs.push(rel);
180 return this;
Akronf5dc5102017-05-16 20:32:57 +0200181 },
182
Akronc5b5f742017-05-23 16:04:35 +0200183
184 addToken : function(token) {
185 this._tokens.push(token);
186 return this;
187 },
188
Akronf5dc5102017-05-16 20:32:57 +0200189 /*
190 * All arcs need to be sorted before shown,
191 * to avoid nesting.
192 */
193 _sortArcs : function () {
194
Akrond67d45b2017-05-18 21:47:38 +0200195
196 // TODO:
197 // Keep in mind that the arcs may have long anchors!
198 // 1. Iterate over all arcs
199 // 2. Sort all multi
200 var anchors = [];
201
Akronf5dc5102017-05-16 20:32:57 +0200202 // 1. Sort by length
203 // 2. Tag all spans with the number of overlaps before
204 // a) Iterate over all spans
205 // b) check the latest preceeding overlapping span (lpos)
206 // -> not found: tag with 0
207 // -> found: Add +1 to the level of the (lpos)
208 // c) If the new tag is smaller than the previous element,
209 // reorder
Akron63ae00b2017-05-16 22:03:36 +0200210
211 // Normalize start and end
212 var sortedArcs = this._arcs.map(function (v) {
Akrond67d45b2017-05-18 21:47:38 +0200213
214 // Check for long anchors
215 if (v.start instanceof Array) {
216 var middle = Math.ceil(Math.abs(v.start[1] - v.start[0]) / 2) + v.start[0];
217
218 v.startAnchor = {
Akron3a4a08e2017-05-23 22:34:18 +0200219 "first": v.start[0],
220 "last" : v.start[1],
Akrond67d45b2017-05-18 21:47:38 +0200221 "length" : v.start[1] - v.start[0]
222 };
223
224 // Add to anchors list
225 anchors.push(v.startAnchor);
226 v.start = middle;
227 };
228
229 if (v.end instanceof Array) {
230 var middle = Math.abs(v.end[0] - v.end[1]) + v.end[0];
231 v.endAnchor = {
Akron3a4a08e2017-05-23 22:34:18 +0200232 "first": v.end[0],
233 "last" : v.end[1],
Akrond67d45b2017-05-18 21:47:38 +0200234 "length" : v.end[1] - v.end[0]
235 };
236
237 // Add to anchors list
238 anchors.push(v.endAnchor);
239 v.end = middle;
240 };
241
242 // calculate the arch length
Akron63ae00b2017-05-16 22:03:36 +0200243 if (v.start < v.end) {
244 v.first = v.start;
245 v.last = v.end;
246 v.length = v.end - v.start;
247 }
248 else {
249 v.first = v.end;
250 v.last = v.start;
251 v.length = v.start - v.end;
252 };
253 return v;
254 });
255
256 // Sort based on length
257 sortedArcs.sort(function (a, b) {
258 if (a.length < b.length)
259 return -1;
260 else
261 return 1;
262 });
263
Akron3a4a08e2017-05-23 22:34:18 +0200264 this._sortedArcs = lengthSort(sortedArcs, false);
Akrond67d45b2017-05-18 21:47:38 +0200265 this._sortedAnchors = lengthSort(anchors, true);
Akronf5dc5102017-05-16 20:32:57 +0200266 },
267
268 show : function () {
269 var svg = this._element;
Akron3a4a08e2017-05-23 22:34:18 +0200270 var height = this.maxArc;
271
Akron3a4a08e2017-05-23 22:34:18 +0200272 // Delete old group
273 if (svg.getElementsByTagName("g")[0] !== undefined) {
274 var group = svg.getElementsByTagName("g")[0];
275 svg.removeChild(group);
276 this._tokenElements = [];
277 };
278
279 var g = svg.appendChild(this._c("g"));
Akronf5dc5102017-05-16 20:32:57 +0200280
281 /*
282 * Generate token list
283 */
Akron3a4a08e2017-05-23 22:34:18 +0200284 var text = g.appendChild(this._c("text"));
285 text.setAttribute("text-anchor", "start");
286 text.setAttribute("y", height);
Akronf5dc5102017-05-16 20:32:57 +0200287
Akron3a4a08e2017-05-23 22:34:18 +0200288 this._y = height - (this.anchorStart);
289
290 var ws = text.appendChild(this._c("tspan"));
291 ws.appendChild(document.createTextNode('\u00A0'));
292 ws.style.textAnchor = "start";
293
Akronf5dc5102017-05-16 20:32:57 +0200294 var lastRight = 0;
295 for (var node_i in this._tokens) {
296 // Append svg
297 var tspan = text.appendChild(this._c("tspan"));
298 tspan.appendChild(document.createTextNode(this._tokens[node_i]));
Akron3a4a08e2017-05-23 22:34:18 +0200299 tspan.setAttribute("text-anchor", "middle");
300
Akronf5dc5102017-05-16 20:32:57 +0200301 this._tokenElements.push(tspan);
302
303 // Add whitespace!
Akron3a4a08e2017-05-23 22:34:18 +0200304 //var ws = text.appendChild(this._c("tspan"));
305 //ws.appendChild(document.createTextNode(" "));
306 // ws.setAttribute("class", "rel-ws");
307 tspan.setAttribute("dx", this.tokenSep);
Akronf5dc5102017-05-16 20:32:57 +0200308 };
309
Akron3a4a08e2017-05-23 22:34:18 +0200310 var arcs = g.appendChild(this._c("g"));
311 arcs.classList.add("arcs");
Akron63ae00b2017-05-16 22:03:36 +0200312
Akron3a4a08e2017-05-23 22:34:18 +0200313 if (this._sortedArcs === undefined) {
314 this._sortArcs();
315 };
Akrond67d45b2017-05-18 21:47:38 +0200316
317 var i;
318 for (i in this._sortedAnchors) {
Akron3a4a08e2017-05-23 22:34:18 +0200319 arcs.appendChild(this._drawAnchor(this._sortedAnchors[i]));
Akrond67d45b2017-05-18 21:47:38 +0200320 };
321
322 for (i in this._sortedArcs) {
Akron3a4a08e2017-05-23 22:34:18 +0200323 arcs.appendChild(this._drawArc(this._sortedArcs[i]));
Akronf5dc5102017-05-16 20:32:57 +0200324 };
Akron3a4a08e2017-05-23 22:34:18 +0200325
326 var width = text.getBoundingClientRect().width;
Akronf3d7d8e2017-05-23 22:52:54 +0200327 svg.setAttribute("width", width + 20);
328 svg.setAttribute("height", height + 20);
Akron3a4a08e2017-05-23 22:34:18 +0200329 svg.setAttribute("class", "relTree");
Akronf5dc5102017-05-16 20:32:57 +0200330 }
Akrond67d45b2017-05-18 21:47:38 +0200331 };
332
333 function lengthSort (list, inclusive) {
334
335 /*
336 * The "inclusive" flag allows to
337 * modify the behaviour for inclusivity check,
338 * e.g. if identical start or endpoints mean overlap or not.
339 */
340
341 var stack = [];
342
343 // Iterate over all definitions
344 for (var i = 0; i < list.length; i++) {
345 var current = list[i];
346
347 // Check the stack order
348 var overlaps = 0;
349
350 for (var j = (stack.length - 1); j >= 0; j--) {
351 var check = stack[j];
352
353 // (a..(b..b)..a)
354 if (current.first <= check.first && current.last >= check.last) {
355 overlaps = check.overlaps + 1;
356 break;
357 }
358
359 // (a..(b..a)..b)
360 else if (current.first <= check.first && current.last >= check.first) {
361
362 if (inclusive || (current.first != check.first && current.last != check.first)) {
363 overlaps = check.overlaps + (current.length == check.length ? 0 : 1);
364 };
365 }
366
367 // (b..(a..b)..a)
368 else if (current.first <= check.last && current.last >= check.last) {
369
370 if (inclusive || (current.first != check.last && current.last != check.last)) {
371 overlaps = check.overlaps + (current.length == check.length ? 0 : 1);
372 };
373 };
374 };
375
376 // Set overlaps
377 current.overlaps = overlaps;
378
379 stack.push(current);
380
381 // Although it is already sorted,
382 // the new item has to be put at the correct place
383 // TODO: Use something like splice() instead
384 stack.sort(function (a,b) {
385 b.overlaps - a.overlaps
386 });
387 };
388
389 return stack;
390 };
Akronf5dc5102017-05-16 20:32:57 +0200391});