blob: 54fd18cf700301d6d70b5d293119b273169b4429 [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
Akron3a4a08e2017-05-23 22:34:18 +0200262 // Delete old group
263 if (svg.getElementsByTagName("g")[0] !== undefined) {
264 var group = svg.getElementsByTagName("g")[0];
265 svg.removeChild(group);
266 this._tokenElements = [];
267 };
268
269 var g = svg.appendChild(this._c("g"));
Akronf5dc5102017-05-16 20:32:57 +0200270
271 /*
272 * Generate token list
273 */
Akron3a4a08e2017-05-23 22:34:18 +0200274 var text = g.appendChild(this._c("text"));
275 text.setAttribute("text-anchor", "start");
276 text.setAttribute("y", height);
Akronf5dc5102017-05-16 20:32:57 +0200277
Akron3a4a08e2017-05-23 22:34:18 +0200278 this._y = height - (this.anchorStart);
279
280 var ws = text.appendChild(this._c("tspan"));
281 ws.appendChild(document.createTextNode('\u00A0'));
282 ws.style.textAnchor = "start";
283
Akronf5dc5102017-05-16 20:32:57 +0200284 var lastRight = 0;
285 for (var node_i in this._tokens) {
286 // Append svg
287 var tspan = text.appendChild(this._c("tspan"));
288 tspan.appendChild(document.createTextNode(this._tokens[node_i]));
Akron3a4a08e2017-05-23 22:34:18 +0200289 tspan.setAttribute("text-anchor", "middle");
290
Akronf5dc5102017-05-16 20:32:57 +0200291 this._tokenElements.push(tspan);
292
293 // Add whitespace!
Akron3a4a08e2017-05-23 22:34:18 +0200294 //var ws = text.appendChild(this._c("tspan"));
295 //ws.appendChild(document.createTextNode(" "));
296 // ws.setAttribute("class", "rel-ws");
297 tspan.setAttribute("dx", this.tokenSep);
Akronf5dc5102017-05-16 20:32:57 +0200298 };
299
Akron3a4a08e2017-05-23 22:34:18 +0200300 var arcs = g.appendChild(this._c("g"));
301 arcs.classList.add("arcs");
Akron63ae00b2017-05-16 22:03:36 +0200302
Akron3a4a08e2017-05-23 22:34:18 +0200303 if (this._sortedArcs === undefined) {
304 this._sortArcs();
305 };
Akrond67d45b2017-05-18 21:47:38 +0200306
307 var i;
308 for (i in this._sortedAnchors) {
Akron3a4a08e2017-05-23 22:34:18 +0200309 arcs.appendChild(this._drawAnchor(this._sortedAnchors[i]));
Akrond67d45b2017-05-18 21:47:38 +0200310 };
311
312 for (i in this._sortedArcs) {
Akron3a4a08e2017-05-23 22:34:18 +0200313 arcs.appendChild(this._drawArc(this._sortedArcs[i]));
Akronf5dc5102017-05-16 20:32:57 +0200314 };
Akron3a4a08e2017-05-23 22:34:18 +0200315
316 var width = text.getBoundingClientRect().width;
Akronf3d7d8e2017-05-23 22:52:54 +0200317 svg.setAttribute("width", width + 20);
318 svg.setAttribute("height", height + 20);
Akron3a4a08e2017-05-23 22:34:18 +0200319 svg.setAttribute("class", "relTree");
Akronf5dc5102017-05-16 20:32:57 +0200320 }
Akrond67d45b2017-05-18 21:47:38 +0200321 };
322
323 function lengthSort (list, inclusive) {
324
325 /*
326 * The "inclusive" flag allows to
327 * modify the behaviour for inclusivity check,
328 * e.g. if identical start or endpoints mean overlap or not.
329 */
330
331 var stack = [];
332
333 // Iterate over all definitions
334 for (var i = 0; i < list.length; i++) {
335 var current = list[i];
336
337 // Check the stack order
338 var overlaps = 0;
339
340 for (var j = (stack.length - 1); j >= 0; j--) {
341 var check = stack[j];
342
343 // (a..(b..b)..a)
344 if (current.first <= check.first && current.last >= check.last) {
345 overlaps = check.overlaps + 1;
346 break;
347 }
348
349 // (a..(b..a)..b)
350 else if (current.first <= check.first && current.last >= check.first) {
351
352 if (inclusive || (current.first != check.first && current.last != check.first)) {
353 overlaps = check.overlaps + (current.length == check.length ? 0 : 1);
354 };
355 }
356
357 // (b..(a..b)..a)
358 else if (current.first <= check.last && current.last >= check.last) {
359
360 if (inclusive || (current.first != check.last && current.last != check.last)) {
361 overlaps = check.overlaps + (current.length == check.length ? 0 : 1);
362 };
363 };
364 };
365
366 // Set overlaps
367 current.overlaps = overlaps;
368
369 stack.push(current);
370
371 // Although it is already sorted,
372 // the new item has to be put at the correct place
373 // TODO: Use something like splice() instead
374 stack.sort(function (a,b) {
375 b.overlaps - a.overlaps
376 });
377 };
378
379 return stack;
380 };
Akronf5dc5102017-05-16 20:32:57 +0200381});