blob: bf80fb309bd094a688af77d49dc629273c063d59 [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));
Akronf5dc5102017-05-16 20:32:57 +0200134 };
Akronf5dc5102017-05-16 20:32:57 +0200135 return g;
136 },
137
138 element : function () {
139 if (this._element !== undefined)
140 return this._element;
141
142 // Create svg
143 var svg = this._c("svg");
Akron3a4a08e2017-05-23 22:34:18 +0200144
145 window.addEventListener("resize", function () {
146 // TODO: Only if text-size changed!
147 this.show();
148 }.bind(this));
149
150 var defs = svg.appendChild(this._c("defs"));
151 var marker = defs.appendChild(this._c("marker"));
152 marker.setAttribute("refX", 9);
153 marker.setAttribute("id", "arr");
154 marker.setAttribute("orient", "auto-start-reverse");
155 marker.setAttribute("markerUnits","userSpaceOnUse");
156
157 var arrow = this._c("path");
158 arrow.setAttribute("transform", "scale(0.8)");
159 arrow.setAttribute("d", "M 0,-5 0,5 10,0 Z");
160 marker.appendChild(arrow);
161
Akronf5dc5102017-05-16 20:32:57 +0200162 this._element = svg;
163 return this._element;
164 },
165
166 // Add a relation with a start, an end,
167 // a direction value and a label text
Akronc5b5f742017-05-23 16:04:35 +0200168 addRel : function (rel) {
169 this._arcs.push(rel);
170 return this;
Akronf5dc5102017-05-16 20:32:57 +0200171 },
172
Akronc5b5f742017-05-23 16:04:35 +0200173
174 addToken : function(token) {
175 this._tokens.push(token);
176 return this;
177 },
178
Akronf5dc5102017-05-16 20:32:57 +0200179 /*
180 * All arcs need to be sorted before shown,
181 * to avoid nesting.
182 */
183 _sortArcs : function () {
184
Akrond67d45b2017-05-18 21:47:38 +0200185
186 // TODO:
187 // Keep in mind that the arcs may have long anchors!
188 // 1. Iterate over all arcs
189 // 2. Sort all multi
190 var anchors = [];
191
Akronf5dc5102017-05-16 20:32:57 +0200192 // 1. Sort by length
193 // 2. Tag all spans with the number of overlaps before
194 // a) Iterate over all spans
195 // b) check the latest preceeding overlapping span (lpos)
196 // -> not found: tag with 0
197 // -> found: Add +1 to the level of the (lpos)
198 // c) If the new tag is smaller than the previous element,
199 // reorder
Akron63ae00b2017-05-16 22:03:36 +0200200
201 // Normalize start and end
202 var sortedArcs = this._arcs.map(function (v) {
Akrond67d45b2017-05-18 21:47:38 +0200203
204 // Check for long anchors
205 if (v.start instanceof Array) {
206 var middle = Math.ceil(Math.abs(v.start[1] - v.start[0]) / 2) + v.start[0];
207
208 v.startAnchor = {
Akron3a4a08e2017-05-23 22:34:18 +0200209 "first": v.start[0],
210 "last" : v.start[1],
Akrond67d45b2017-05-18 21:47:38 +0200211 "length" : v.start[1] - v.start[0]
212 };
213
214 // Add to anchors list
215 anchors.push(v.startAnchor);
216 v.start = middle;
217 };
218
219 if (v.end instanceof Array) {
220 var middle = Math.abs(v.end[0] - v.end[1]) + v.end[0];
221 v.endAnchor = {
Akron3a4a08e2017-05-23 22:34:18 +0200222 "first": v.end[0],
223 "last" : v.end[1],
Akrond67d45b2017-05-18 21:47:38 +0200224 "length" : v.end[1] - v.end[0]
225 };
226
227 // Add to anchors list
228 anchors.push(v.endAnchor);
229 v.end = middle;
230 };
231
232 // calculate the arch length
Akron63ae00b2017-05-16 22:03:36 +0200233 if (v.start < v.end) {
234 v.first = v.start;
235 v.last = v.end;
236 v.length = v.end - v.start;
237 }
238 else {
239 v.first = v.end;
240 v.last = v.start;
241 v.length = v.start - v.end;
242 };
243 return v;
244 });
245
246 // Sort based on length
247 sortedArcs.sort(function (a, b) {
248 if (a.length < b.length)
249 return -1;
250 else
251 return 1;
252 });
253
Akron3a4a08e2017-05-23 22:34:18 +0200254 this._sortedArcs = lengthSort(sortedArcs, false);
Akrond67d45b2017-05-18 21:47:38 +0200255 this._sortedAnchors = lengthSort(anchors, true);
Akronf5dc5102017-05-16 20:32:57 +0200256 },
257
258 show : function () {
259 var svg = this._element;
Akron3a4a08e2017-05-23 22:34:18 +0200260 var height = this.maxArc;
261
262 /*
263 svg.setAttribute("width", 700);
264 svg.setAttribute("height", 300);
265 */
266
267 // Delete old group
268 if (svg.getElementsByTagName("g")[0] !== undefined) {
269 var group = svg.getElementsByTagName("g")[0];
270 svg.removeChild(group);
271 this._tokenElements = [];
272 };
273
274 var g = svg.appendChild(this._c("g"));
Akronf5dc5102017-05-16 20:32:57 +0200275
276 /*
277 * Generate token list
278 */
Akron3a4a08e2017-05-23 22:34:18 +0200279 var text = g.appendChild(this._c("text"));
280 text.setAttribute("text-anchor", "start");
281 text.setAttribute("y", height);
Akronf5dc5102017-05-16 20:32:57 +0200282
Akron3a4a08e2017-05-23 22:34:18 +0200283 this._y = height - (this.anchorStart);
284
285 var ws = text.appendChild(this._c("tspan"));
286 ws.appendChild(document.createTextNode('\u00A0'));
287 ws.style.textAnchor = "start";
288
Akronf5dc5102017-05-16 20:32:57 +0200289 var lastRight = 0;
290 for (var node_i in this._tokens) {
291 // Append svg
292 var tspan = text.appendChild(this._c("tspan"));
293 tspan.appendChild(document.createTextNode(this._tokens[node_i]));
Akron3a4a08e2017-05-23 22:34:18 +0200294 tspan.setAttribute("text-anchor", "middle");
295
Akronf5dc5102017-05-16 20:32:57 +0200296 this._tokenElements.push(tspan);
297
298 // Add whitespace!
Akron3a4a08e2017-05-23 22:34:18 +0200299 //var ws = text.appendChild(this._c("tspan"));
300 //ws.appendChild(document.createTextNode(" "));
301 // ws.setAttribute("class", "rel-ws");
302 tspan.setAttribute("dx", this.tokenSep);
Akronf5dc5102017-05-16 20:32:57 +0200303 };
304
Akron3a4a08e2017-05-23 22:34:18 +0200305 var arcs = g.appendChild(this._c("g"));
306 arcs.classList.add("arcs");
Akronf5dc5102017-05-16 20:32:57 +0200307
Akron3a4a08e2017-05-23 22:34:18 +0200308 /*
Akronf5dc5102017-05-16 20:32:57 +0200309 var textBox = text.getBoundingClientRect();
310
Akron3a4a08e2017-05-23 22:34:18 +0200311 arcs.setAttribute(
Akronf5dc5102017-05-16 20:32:57 +0200312 "transform",
313 "translate(0," + textBox.y +")"
314 );
Akron3a4a08e2017-05-23 22:34:18 +0200315 */
Akron63ae00b2017-05-16 22:03:36 +0200316
Akronf5dc5102017-05-16 20:32:57 +0200317 /*
318 * TODO:
319 * Before creating the arcs, the height of the arc
320 * needs to be calculated to make it possible to "stack" arcs.
321 * That means, the arcs need to be presorted, so massively
322 * overlapping arcs are taken first.
Akrond67d45b2017-05-18 21:47:38 +0200323 * On the other hand, anchors need to be sorted as well
324 * in the same way.
Akronf5dc5102017-05-16 20:32:57 +0200325 */
Akron3a4a08e2017-05-23 22:34:18 +0200326 if (this._sortedArcs === undefined) {
327 this._sortArcs();
328 };
Akrond67d45b2017-05-18 21:47:38 +0200329
330 var i;
331 for (i in this._sortedAnchors) {
Akron3a4a08e2017-05-23 22:34:18 +0200332 arcs.appendChild(this._drawAnchor(this._sortedAnchors[i]));
Akrond67d45b2017-05-18 21:47:38 +0200333 };
334
335 for (i in this._sortedArcs) {
Akron3a4a08e2017-05-23 22:34:18 +0200336 arcs.appendChild(this._drawArc(this._sortedArcs[i]));
Akronf5dc5102017-05-16 20:32:57 +0200337 };
Akron3a4a08e2017-05-23 22:34:18 +0200338
339 var width = text.getBoundingClientRect().width;
340 svg.setAttribute("width", width);
341 svg.setAttribute("height", height);
342 svg.setAttribute("class", "relTree");
343
344 // svg.setAttribute("viewbox", "0 0 500 200");
345 /*
346 console.log(size.width);
347 console.log(size.height);
348 */
Akronf5dc5102017-05-16 20:32:57 +0200349 }
Akrond67d45b2017-05-18 21:47:38 +0200350 };
351
352 function lengthSort (list, inclusive) {
353
354 /*
355 * The "inclusive" flag allows to
356 * modify the behaviour for inclusivity check,
357 * e.g. if identical start or endpoints mean overlap or not.
358 */
359
360 var stack = [];
361
362 // Iterate over all definitions
363 for (var i = 0; i < list.length; i++) {
364 var current = list[i];
365
366 // Check the stack order
367 var overlaps = 0;
368
369 for (var j = (stack.length - 1); j >= 0; j--) {
370 var check = stack[j];
371
372 // (a..(b..b)..a)
373 if (current.first <= check.first && current.last >= check.last) {
374 overlaps = check.overlaps + 1;
375 break;
376 }
377
378 // (a..(b..a)..b)
379 else if (current.first <= check.first && current.last >= check.first) {
380
381 if (inclusive || (current.first != check.first && current.last != check.first)) {
382 overlaps = check.overlaps + (current.length == check.length ? 0 : 1);
383 };
384 }
385
386 // (b..(a..b)..a)
387 else if (current.first <= check.last && current.last >= check.last) {
388
389 if (inclusive || (current.first != check.last && current.last != check.last)) {
390 overlaps = check.overlaps + (current.length == check.length ? 0 : 1);
391 };
392 };
393 };
394
395 // Set overlaps
396 current.overlaps = overlaps;
397
398 stack.push(current);
399
400 // Although it is already sorted,
401 // the new item has to be put at the correct place
402 // TODO: Use something like splice() instead
403 stack.sort(function (a,b) {
404 b.overlaps - a.overlaps
405 });
406 };
407
408 return stack;
409 };
Akronf5dc5102017-05-16 20:32:57 +0200410});