blob: 28a279b42f35c45b31c7cc20ea95f6fcb1d9d9a4 [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 = [];
Akronf5dc5102017-05-16 20:32:57 +020010 obj._tokenElements = [];
Akronc5b5f742017-05-23 16:04:35 +020011 obj._arcs = []
Akrond67d45b2017-05-18 21:47:38 +020012
Akronc5b5f742017-05-23 16:04:35 +020013 // Some configurations
Akronf5dc5102017-05-16 20:32:57 +020014 obj.maxArc = 200; // maximum height of the bezier control point
Akronc5b5f742017-05-23 16:04:35 +020015 obj.overlapDiff = 20;
16 obj.arcDiff = 15;
17 obj.anchorDiff = 6;
18 obj.anchorStart = 10;
19 obj.tokenSep = 30;
Akronf5dc5102017-05-16 20:32:57 +020020 return obj;
21 },
22
23 _init : function (snippet) {
24 /*
25 var html = document.createElement("div");
26 html.innerHTML = snippet;
27 */
28 return this;
29 },
30
31 // This is a shorthand for SVG element creation
32 _c : function (tag) {
33 return document.createElementNS(svgNS, tag);
34 },
35
36 // Returns the center point of the requesting token
37 _tokenPoint : function (node) {
38 var box = node.getBoundingClientRect();
39 return box.x + (box.width / 2);
40 },
41
Akrond67d45b2017-05-18 21:47:38 +020042 _drawAnchor : function (anchor) {
43 var startPos = this._tokenElements[anchor.first].getBoundingClientRect().left;
44 var endPos = this._tokenElements[anchor.last].getBoundingClientRect().right;
45
Akronc5b5f742017-05-23 16:04:35 +020046 var y = (anchor.overlaps * -1 * this.anchorDiff) - this.anchorStart; // - this.arcDiff;
Akrond67d45b2017-05-18 21:47:38 +020047 var l = this._c('path');
48 l.setAttribute("d", "M " + startPos + " " + y + " L " + endPos + " " + y);
49 l.setAttribute("class", "anchor");
50 anchor.element = l;
51 anchor.y = y;
52 return l;
53 },
54
Akronf5dc5102017-05-16 20:32:57 +020055 // Create an arc with a label
56 // Potentially needs a height parameter for stacks
Akron63ae00b2017-05-16 22:03:36 +020057 _drawArc : function (arc) {
Akronf5dc5102017-05-16 20:32:57 +020058
Akrond67d45b2017-05-18 21:47:38 +020059 var startPos, endPos;
60 var startY = 0, endY = 0;
Akronf5dc5102017-05-16 20:32:57 +020061
Akrond67d45b2017-05-18 21:47:38 +020062 if (arc.startAnchor !== undefined) {
63 startPos = this._tokenPoint(arc.startAnchor.element)
64 startY = arc.startAnchor.y;
65 }
66 else {
67 startPos = this._tokenPoint(this._tokenElements[arc.first]);
68 };
69
70 if (arc.endAnchor !== undefined) {
71 endPos = this._tokenPoint(arc.endAnchor.element)
72 endY = arc.endAnchor.y;
73 }
74 else {
75 endPos = this._tokenPoint(this._tokenElements[arc.last]);
76 };
77
Akronf5dc5102017-05-16 20:32:57 +020078 var g = this._c("g");
79 var p = g.appendChild(this._c("path"));
80
81 // Create arc
82 var middle = Math.abs(endPos - startPos) / 2;
83
Akron63ae00b2017-05-16 22:03:36 +020084 // TODO: take the number of tokens into account!
Akronc5b5f742017-05-23 16:04:35 +020085 var cHeight = this.arcDiff + arc.overlaps * this.overlapDiff + (middle / 2);
86
87 // Respect the maximum height
88 cHeight = cHeight < this.maxArc ? cHeight : this.maxArc;
Akronf5dc5102017-05-16 20:32:57 +020089
90 var x = Math.min(startPos, endPos);
Akronc5b5f742017-05-23 16:04:35 +020091
92 var controlY = (startY + endY - cHeight);
Akronf5dc5102017-05-16 20:32:57 +020093
Akrond67d45b2017-05-18 21:47:38 +020094 var arcE = "M "+ startPos + " " + startY +
Akronc5b5f742017-05-23 16:04:35 +020095 " C " + startPos + " " + controlY +
96 " " + endPos + " " + controlY +
Akrond67d45b2017-05-18 21:47:38 +020097 " " + endPos + " " + endY;
98
Akron63ae00b2017-05-16 22:03:36 +020099 p.setAttribute("d", arcE);
Akronf5dc5102017-05-16 20:32:57 +0200100
Akronc5b5f742017-05-23 16:04:35 +0200101 /*
102 * Calculate the top point of the arc for labeling using
103 * de Casteljau's algorithm, see e.g.
104 * http://blog.sklambert.com/finding-the-control-points-of-a-bezier-curve/
105 * of course simplified to symmetric arcs ...
106 */
107 // Interpolate one side of the control polygon
108 // var controlInterpY1 = (startY + controlY) / 2;
109 // var controlInterpY2 = (controlInterpY1 + controlY) / 2;
110 var middleY = (((startY + controlY) / 2) + controlY) / 2;
111
112 // WARNING!
113 // This won't respect span anchors, adjusting startY and endY!
114
Akron63ae00b2017-05-16 22:03:36 +0200115 if (arc.label !== undefined) {
Akronf5dc5102017-05-16 20:32:57 +0200116 var labelE = g.appendChild(this._c("text"));
117 labelE.setAttribute("x", x + middle);
Akronc5b5f742017-05-23 16:04:35 +0200118 labelE.setAttribute("y", middleY + 3);
Akronf5dc5102017-05-16 20:32:57 +0200119 labelE.setAttribute("text-anchor", "middle");
Akron63ae00b2017-05-16 22:03:36 +0200120 labelE.appendChild(document.createTextNode(arc.label));
Akronf5dc5102017-05-16 20:32:57 +0200121 };
Akronc5b5f742017-05-23 16:04:35 +0200122
123 /*
124 var circle = this._c("circle");
125 circle.setAttribute("cx", x + middle);
126 circle.setAttribute("cy", middleY);
127 circle.setAttribute("r", 4);
128 circle.setAttribute("fill", "red");
129 g.appendChild(circle);
130 */
Akronf5dc5102017-05-16 20:32:57 +0200131 return g;
132 },
133
134 element : function () {
135 if (this._element !== undefined)
136 return this._element;
137
138 // Create svg
139 var svg = this._c("svg");
140 svg.setAttribute("width", 700);
141 svg.setAttribute("height", 300);
142 this._element = svg;
143 return this._element;
144 },
145
146 // Add a relation with a start, an end,
147 // a direction value and a label text
Akronc5b5f742017-05-23 16:04:35 +0200148 addRel : function (rel) {
149 this._arcs.push(rel);
150 return this;
Akronf5dc5102017-05-16 20:32:57 +0200151 },
152
Akronc5b5f742017-05-23 16:04:35 +0200153
154 addToken : function(token) {
155 this._tokens.push(token);
156 return this;
157 },
158
Akronf5dc5102017-05-16 20:32:57 +0200159 /*
160 * All arcs need to be sorted before shown,
161 * to avoid nesting.
162 */
163 _sortArcs : function () {
164
Akrond67d45b2017-05-18 21:47:38 +0200165
166 // TODO:
167 // Keep in mind that the arcs may have long anchors!
168 // 1. Iterate over all arcs
169 // 2. Sort all multi
170 var anchors = [];
171
Akronf5dc5102017-05-16 20:32:57 +0200172 // 1. Sort by length
173 // 2. Tag all spans with the number of overlaps before
174 // a) Iterate over all spans
175 // b) check the latest preceeding overlapping span (lpos)
176 // -> not found: tag with 0
177 // -> found: Add +1 to the level of the (lpos)
178 // c) If the new tag is smaller than the previous element,
179 // reorder
Akron63ae00b2017-05-16 22:03:36 +0200180
181 // Normalize start and end
182 var sortedArcs = this._arcs.map(function (v) {
Akrond67d45b2017-05-18 21:47:38 +0200183
184 // Check for long anchors
185 if (v.start instanceof Array) {
186 var middle = Math.ceil(Math.abs(v.start[1] - v.start[0]) / 2) + v.start[0];
187
188 v.startAnchor = {
189 "first": v.start[0],
190 "last" : v.start[1],
191 "length" : v.start[1] - v.start[0]
192 };
193
194 // Add to anchors list
195 anchors.push(v.startAnchor);
196 v.start = middle;
197 };
198
199 if (v.end instanceof Array) {
200 var middle = Math.abs(v.end[0] - v.end[1]) + v.end[0];
201 v.endAnchor = {
202 "first": v.end[0],
203 "last" : v.end[1],
204 "length" : v.end[1] - v.end[0]
205 };
206
207 // Add to anchors list
208 anchors.push(v.endAnchor);
209 v.end = middle;
210 };
211
212 // calculate the arch length
Akron63ae00b2017-05-16 22:03:36 +0200213 if (v.start < v.end) {
214 v.first = v.start;
215 v.last = v.end;
216 v.length = v.end - v.start;
217 }
218 else {
219 v.first = v.end;
220 v.last = v.start;
221 v.length = v.start - v.end;
222 };
223 return v;
224 });
225
226 // Sort based on length
227 sortedArcs.sort(function (a, b) {
228 if (a.length < b.length)
229 return -1;
230 else
231 return 1;
232 });
233
Akrond67d45b2017-05-18 21:47:38 +0200234 this._sortedArcs = lengthSort(sortedArcs, false);
Akron63ae00b2017-05-16 22:03:36 +0200235
Akrond67d45b2017-05-18 21:47:38 +0200236 this._sortedAnchors = lengthSort(anchors, true);
Akronf5dc5102017-05-16 20:32:57 +0200237 },
238
239 show : function () {
240 var svg = this._element;
241
242 /*
243 * Generate token list
244 */
245 var text = svg.appendChild(this._c("text"));
246 text.setAttribute("y", 135);
247 text.setAttribute("x", 160);
248
249 var lastRight = 0;
250 for (var node_i in this._tokens) {
251 // Append svg
252 var tspan = text.appendChild(this._c("tspan"));
253 tspan.appendChild(document.createTextNode(this._tokens[node_i]));
254
255 this._tokenElements.push(tspan);
256
257 // Add whitespace!
Akronc5b5f742017-05-23 16:04:35 +0200258 // var whitespace = text.appendChild(document.createTextNode(" "));
259 var ws = text.appendChild(this._c("tspan"));
260 ws.appendChild(document.createTextNode(" "));
261 ws.setAttribute("class", "rel-ws");
262 ws.setAttribute("dx", this.tokenSep);
Akronf5dc5102017-05-16 20:32:57 +0200263 };
264
265 this.arcs = svg.appendChild(this._c("g"));
266 this.arcs.classList.add("arcs");
267
268 var textBox = text.getBoundingClientRect();
269
270 this.arcs.setAttribute(
271 "transform",
272 "translate(0," + textBox.y +")"
273 );
Akron63ae00b2017-05-16 22:03:36 +0200274
Akronf5dc5102017-05-16 20:32:57 +0200275 /*
276 * TODO:
277 * Before creating the arcs, the height of the arc
278 * needs to be calculated to make it possible to "stack" arcs.
279 * That means, the arcs need to be presorted, so massively
280 * overlapping arcs are taken first.
Akrond67d45b2017-05-18 21:47:38 +0200281 * On the other hand, anchors need to be sorted as well
282 * in the same way.
Akronf5dc5102017-05-16 20:32:57 +0200283 */
Akrond67d45b2017-05-18 21:47:38 +0200284 this._sortArcs();
285
286 var i;
287 for (i in this._sortedAnchors) {
288 this.arcs.appendChild(this._drawAnchor(this._sortedAnchors[i]));
289 };
290
291 for (i in this._sortedArcs) {
292 this.arcs.appendChild(this._drawArc(this._sortedArcs[i]));
Akronf5dc5102017-05-16 20:32:57 +0200293 };
294 }
Akrond67d45b2017-05-18 21:47:38 +0200295 };
296
297 function lengthSort (list, inclusive) {
298
299 /*
300 * The "inclusive" flag allows to
301 * modify the behaviour for inclusivity check,
302 * e.g. if identical start or endpoints mean overlap or not.
303 */
304
305 var stack = [];
306
307 // Iterate over all definitions
308 for (var i = 0; i < list.length; i++) {
309 var current = list[i];
310
311 // Check the stack order
312 var overlaps = 0;
313
314 for (var j = (stack.length - 1); j >= 0; j--) {
315 var check = stack[j];
316
317 // (a..(b..b)..a)
318 if (current.first <= check.first && current.last >= check.last) {
319 overlaps = check.overlaps + 1;
320 break;
321 }
322
323 // (a..(b..a)..b)
324 else if (current.first <= check.first && current.last >= check.first) {
325
326 if (inclusive || (current.first != check.first && current.last != check.first)) {
327 overlaps = check.overlaps + (current.length == check.length ? 0 : 1);
328 };
329 }
330
331 // (b..(a..b)..a)
332 else if (current.first <= check.last && current.last >= check.last) {
333
334 if (inclusive || (current.first != check.last && current.last != check.last)) {
335 overlaps = check.overlaps + (current.length == check.length ? 0 : 1);
336 };
337 };
338 };
339
340 // Set overlaps
341 current.overlaps = overlaps;
342
343 stack.push(current);
344
345 // Although it is already sorted,
346 // the new item has to be put at the correct place
347 // TODO: Use something like splice() instead
348 stack.sort(function (a,b) {
349 b.overlaps - a.overlaps
350 });
351 };
352
353 return stack;
354 };
Akronf5dc5102017-05-16 20:32:57 +0200355});