blob: 6f69cbbfaff321256ae40fa3e426d6ffbe183472 [file] [log] [blame]
Akron6f1302b2017-09-13 12:46:02 +02001/**
2 * Parse a relational tree and visualize using arcs.
3 *
4 * @author Nils Diewald
5 */
6
Akron671fdb92017-09-12 18:09:46 +02007define([], function () {
Akronf5dc5102017-05-16 20:32:57 +02008 "use strict";
9
10 var svgNS = "http://www.w3.org/2000/svg";
Akron671fdb92017-09-12 18:09:46 +020011 var _TermRE = new RegExp("^(?:([^\/]+?)\/)?([^:]+?):(.+?)$");
Akronf5dc5102017-05-16 20:32:57 +020012
13 return {
14 create : function (snippet) {
Akron6f1302b2017-09-13 12:46:02 +020015 return Object.create(this)._init(snippet);
Akronf5dc5102017-05-16 20:32:57 +020016 },
Akron6f1302b2017-09-13 12:46:02 +020017
18 // Initialize the state of the object
Akronf5dc5102017-05-16 20:32:57 +020019 _init : function (snippet) {
Akron671fdb92017-09-12 18:09:46 +020020
Akron6f1302b2017-09-13 12:46:02 +020021 // Predefine some values
Akron71c87de2017-09-15 14:24:21 +020022 this._tokens = [];
23 this._arcs = [];
Akron6f1302b2017-09-13 12:46:02 +020024 this._tokenElements = [];
25 this._y = 0;
Akron671fdb92017-09-12 18:09:46 +020026
Akron6f1302b2017-09-13 12:46:02 +020027 // Some configurations
28 this.maxArc = 200; // maximum height of the bezier control point
Akronfee0b622017-09-13 14:46:43 +020029 this.overlapDiff = 40; // Difference on overlaps and minimum height for self-refernces
Akron6f1302b2017-09-13 12:46:02 +020030 this.arcDiff = 15;
31 this.anchorDiff = 8;
32 this.anchorStart = 15;
33 this.tokenSep = 30;
34 this.xPadding = 10;
35 this.yPadding = 5;
Akron671fdb92017-09-12 18:09:46 +020036
Akron6f1302b2017-09-13 12:46:02 +020037 // No snippet to process
38 if (snippet == undefined || snippet == null)
39 return this;
Akron671fdb92017-09-12 18:09:46 +020040
Akron6f1302b2017-09-13 12:46:02 +020041 // Parse the snippet
42 var html = document.createElement("div");
43 html.innerHTML = snippet;
Akron671fdb92017-09-12 18:09:46 +020044
Akron6f1302b2017-09-13 12:46:02 +020045 // Establish temporary parsing memory
46 this.temp = {
47 target : {}, // Remember the map id => pos
48 edges : [], // Remember edge definitions
49 pos : 0 // Keep track of the current token position
Akron671fdb92017-09-12 18:09:46 +020050 };
51
Akron6f1302b2017-09-13 12:46:02 +020052 // Start parsing from root
53 this._parse(0, html.childNodes, undefined);
54
55 // Establish edge list
56 var targetMap = this.temp['target'];
57 var edges = this.temp['edges'];
58
59 // Iterate over edge lists
60 // TODO:
61 // Support spans for anchors!
62 for (var i in edges) {
63 var edge = edges[i];
64
65 // Check the target identifier
66 var targetID = edge.targetID;
67 var target = targetMap[targetID];
68
69 if (target != undefined) {
70
Akron2c1d9ff2017-09-15 13:57:33 +020071 // Check if the source is a span anchor
Akron0d04a182017-09-15 14:34:50 +020072 /*
Akron2c1d9ff2017-09-15 13:57:33 +020073 var start = edge.srcStart;
74 if (start !== edge.srcEnd) {
75 start = [start, edge.srcEnd];
76 };
Akron0d04a182017-09-15 14:34:50 +020077 */
Akron2c1d9ff2017-09-15 13:57:33 +020078
Akron6f1302b2017-09-13 12:46:02 +020079 // Add relation
Akron2c1d9ff2017-09-15 13:57:33 +020080 var relation = {
Akron0d04a182017-09-15 14:34:50 +020081 start : [edge.srcStart, edge.srcEnd],
Akron6f1302b2017-09-13 12:46:02 +020082 end : target,
83 direction : 'uni',
84 label : edge.label
Akron2c1d9ff2017-09-15 13:57:33 +020085 };
86 // console.log(relation);
87 this.addRel(relation);
Akron6f1302b2017-09-13 12:46:02 +020088 };
89 };
90
91 // Reset parsing memory
92 this.temp = {};
93
Akronf5dc5102017-05-16 20:32:57 +020094 return this;
95 },
96
Akron6f1302b2017-09-13 12:46:02 +020097 // Parse a node of the tree snippet
Akron671fdb92017-09-12 18:09:46 +020098 _parse : function (parent, children, mark) {
Akron6f1302b2017-09-13 12:46:02 +020099
100 // Iterate over all child nodes
Akron671fdb92017-09-12 18:09:46 +0200101 for (var i in children) {
102 var c = children[i];
103
104 // Element node
105 if (c.nodeType == 1) {
106
Akron2c1d9ff2017-09-15 13:57:33 +0200107 var xmlid, target;
108
Akron671fdb92017-09-12 18:09:46 +0200109 // Node is an identifier
110 if (c.hasAttribute('xml:id')) {
111
112 // Remember that pos has this identifier
Akron2c1d9ff2017-09-15 13:57:33 +0200113 xmlid = c.getAttribute('xml:id');
114 this.temp['target'][xmlid] = [this.temp['pos'], this.temp['pos']];
Akron671fdb92017-09-12 18:09:46 +0200115 }
116
117 // Node is a relation
118 else if (c.hasAttribute('xlink:href')) {
Akron6f1302b2017-09-13 12:46:02 +0200119 var label;
Akron671fdb92017-09-12 18:09:46 +0200120
Akron6f1302b2017-09-13 12:46:02 +0200121 // Get target id
Akron2c1d9ff2017-09-15 13:57:33 +0200122 target = c.getAttribute('xlink:href').replace(/^#/, "");
Akron671fdb92017-09-12 18:09:46 +0200123
124 if (c.hasAttribute('xlink:title')) {
125 label = this._clean(c.getAttribute('xlink:title'));
126 };
127
128 // Remember the defined edge
Akron2c1d9ff2017-09-15 13:57:33 +0200129 var edge = {
Akron6f1302b2017-09-13 12:46:02 +0200130 label : label,
Akron2c1d9ff2017-09-15 13:57:33 +0200131 srcStart : this.temp['pos'],
Akron671fdb92017-09-12 18:09:46 +0200132 targetID : target
Akron2c1d9ff2017-09-15 13:57:33 +0200133 };
134 this.temp['edges'].push(edge);
Akron671fdb92017-09-12 18:09:46 +0200135 };
136
Akron6f1302b2017-09-13 12:46:02 +0200137 // Go on with child nodes
Akron671fdb92017-09-12 18:09:46 +0200138 if (c.hasChildNodes()) {
139 this._parse(0, c.childNodes, mark);
140 };
Akron2c1d9ff2017-09-15 13:57:33 +0200141
142 if (xmlid !== undefined) {
143 this.temp['target'][xmlid][1] = this.temp['pos'] -1;
144
145 /*
146 console.log('Target ' + xmlid + ' spans from ' +
147 this.temp['target'][xmlid][0] +
148 ' to ' +
149 this.temp['target'][xmlid][1]
150 );
151 */
152 xmlid = undefined;
153 }
154 else if (target !== undefined) {
155 edge["srcEnd"] = this.temp['pos'] -1;
156
157 /*
158 console.log('Source spans from ' +
159 edge["srcStart"] +
160 ' to ' +
161 edge["srcEnd"]
162 );
163 */
164 target = undefined;
165 };
Akron671fdb92017-09-12 18:09:46 +0200166 }
167
168 // Text node
169 else if (c.nodeType == 3) {
170
171 // Check, if there is a non-whitespace token
172 if (c.nodeValue !== undefined) {
173 var str = c.nodeValue.trim();
174 if (str !== undefined && str.length > 0) {
175
176 // Add token to token list
177 this.addToken(str);
178
179 // Move token position
180 this.temp['pos']++;
181 };
182 };
183 }
184 };
185 },
186
Akron6f1302b2017-09-13 12:46:02 +0200187
Akron671fdb92017-09-12 18:09:46 +0200188 // Remove foundry and layer for labels
189 _clean : function (title) {
190 return title.replace(_TermRE, "$3");
191 },
192
Akron6f1302b2017-09-13 12:46:02 +0200193
194 // Return the number of leaf nodes
195 // (not necessarily part of a relation).
196 // Consecutive nodes that are not part of any
197 // relation are summarized in one node.
Akron671fdb92017-09-12 18:09:46 +0200198 size : function () {
199 return this._tokens.length;
200 },
Akron15175132017-09-07 18:12:55 +0200201
Akron6f1302b2017-09-13 12:46:02 +0200202
Akronf5dc5102017-05-16 20:32:57 +0200203 // This is a shorthand for SVG element creation
204 _c : function (tag) {
205 return document.createElementNS(svgNS, tag);
206 },
207
Akron6c3e5ea2017-11-11 07:16:54 +0100208 // Get bounding box - with workaround for text nodes
209 _rect : function (node) {
210 if (node.tagName == "tspan") {
211 var range = document.createRange();
212 range.selectNode(node);
213 var rect = range.getBoundingClientRect();
214 range.detach();
215 return rect;
216 };
217 return node.getBoundingClientRect();
218 },
Akron15175132017-09-07 18:12:55 +0200219
Akronf5dc5102017-05-16 20:32:57 +0200220 // Returns the center point of the requesting token
221 _tokenPoint : function (node) {
Akron6c3e5ea2017-11-11 07:16:54 +0100222 var box = this._rect(node);
Akronf5dc5102017-05-16 20:32:57 +0200223 return box.x + (box.width / 2);
224 },
225
Akron15175132017-09-07 18:12:55 +0200226
227 // Draws an anchor
Akrond67d45b2017-05-18 21:47:38 +0200228 _drawAnchor : function (anchor) {
Akron15175132017-09-07 18:12:55 +0200229
230 // Calculate the span of the first and last token, the anchor spans
Akron6c3e5ea2017-11-11 07:16:54 +0100231 var firstBox = this._rect(this._tokenElements[anchor.first]);
232 var lastBox = this._rect(this._tokenElements[anchor.last]);
Akrond67d45b2017-05-18 21:47:38 +0200233
Akron15175132017-09-07 18:12:55 +0200234 var startPos = firstBox.left - this.offsetLeft;
Akron6f1302b2017-09-13 12:46:02 +0200235 var endPos = lastBox.right - this.offsetLeft;
Akron15175132017-09-07 18:12:55 +0200236
Akron3a4a08e2017-05-23 22:34:18 +0200237 var y = this._y + (anchor.overlaps * this.anchorDiff) - this.anchorStart;
238
Akrond67d45b2017-05-18 21:47:38 +0200239 var l = this._c('path');
Akron65d31082017-09-08 16:23:40 +0200240 this._arcsElement.appendChild(l);
Akron3a4a08e2017-05-23 22:34:18 +0200241 l.setAttribute("d", "M " + startPos + "," + y + " L " + endPos + "," + y);
Akrond67d45b2017-05-18 21:47:38 +0200242 l.setAttribute("class", "anchor");
243 anchor.element = l;
244 anchor.y = y;
245 return l;
246 },
247
Akron6f1302b2017-09-13 12:46:02 +0200248
249 // Create an arc with an optional label
Akronf5dc5102017-05-16 20:32:57 +0200250 // Potentially needs a height parameter for stacks
Akron63ae00b2017-05-16 22:03:36 +0200251 _drawArc : function (arc) {
Akronf5dc5102017-05-16 20:32:57 +0200252
Akrond67d45b2017-05-18 21:47:38 +0200253 var startPos, endPos;
Akronfee0b622017-09-13 14:46:43 +0200254 var startY = this._y;
255 var endY = this._y;
Akronf5dc5102017-05-16 20:32:57 +0200256
Akrond67d45b2017-05-18 21:47:38 +0200257 if (arc.startAnchor !== undefined) {
Akron3a4a08e2017-05-23 22:34:18 +0200258 startPos = this._tokenPoint(arc.startAnchor.element);
Akrond67d45b2017-05-18 21:47:38 +0200259 startY = arc.startAnchor.y;
260 }
261 else {
262 startPos = this._tokenPoint(this._tokenElements[arc.first]);
263 };
264
265 if (arc.endAnchor !== undefined) {
266 endPos = this._tokenPoint(arc.endAnchor.element)
267 endY = arc.endAnchor.y;
268 }
269 else {
270 endPos = this._tokenPoint(this._tokenElements[arc.last]);
271 };
272
Akron15175132017-09-07 18:12:55 +0200273 startPos -= this.offsetLeft;
274 endPos -= this.offsetLeft;
275
Akronfee0b622017-09-13 14:46:43 +0200276 // Special treatment for self-references
277 var overlaps = arc.overlaps;
278 if (startPos == endPos) {
279 startPos -= this.overlapDiff / 3;
Akronc9f23022017-09-13 15:43:52 +0200280 endPos += this.overlapDiff / 3;
Akronfee0b622017-09-13 14:46:43 +0200281 overlaps += .5;
282 };
283
Akronf5dc5102017-05-16 20:32:57 +0200284 var g = this._c("g");
Akron65d31082017-09-08 16:23:40 +0200285 g.setAttribute("class", "arc");
Akronf5dc5102017-05-16 20:32:57 +0200286 var p = g.appendChild(this._c("path"));
Akron15175132017-09-07 18:12:55 +0200287 p.setAttribute('class', 'edge');
Akron65d31082017-09-08 16:23:40 +0200288
289 // Attach the new arc before drawing, so computed values are available
290 this._arcsElement.appendChild(g);
Akronf5dc5102017-05-16 20:32:57 +0200291
292 // Create arc
293 var middle = Math.abs(endPos - startPos) / 2;
294
Akronfee0b622017-09-13 14:46:43 +0200295 // TODO:
296 // take the number of tokens into account!
297 var cHeight = this.arcDiff + (overlaps * this.overlapDiff) + (middle / 2);
Akronc5b5f742017-05-23 16:04:35 +0200298
299 // Respect the maximum height
300 cHeight = cHeight < this.maxArc ? cHeight : this.maxArc;
Akronf5dc5102017-05-16 20:32:57 +0200301
302 var x = Math.min(startPos, endPos);
Akronc5b5f742017-05-23 16:04:35 +0200303
Akron3a4a08e2017-05-23 22:34:18 +0200304 //var controlY = (startY + endY - cHeight);
305 var controlY = (endY - cHeight);
Akronf5dc5102017-05-16 20:32:57 +0200306
Akron3a4a08e2017-05-23 22:34:18 +0200307 var arcE = "M "+ startPos + "," + startY +
308 " C " + startPos + "," + controlY +
309 " " + endPos + "," + controlY +
310 " " + endPos + "," + endY;
Akrond67d45b2017-05-18 21:47:38 +0200311
Akron63ae00b2017-05-16 22:03:36 +0200312 p.setAttribute("d", arcE);
Akronf5dc5102017-05-16 20:32:57 +0200313
Akron3a4a08e2017-05-23 22:34:18 +0200314 if (arc.direction !== undefined) {
315 p.setAttribute("marker-end", "url(#arr)");
316 if (arc.direction === 'bi') {
317 p.setAttribute("marker-start", "url(#arr)");
318 };
319 };
320
Akron6f1302b2017-09-13 12:46:02 +0200321 if (arc.label === undefined)
322 return g;
323
Akronc5b5f742017-05-23 16:04:35 +0200324 /*
325 * Calculate the top point of the arc for labeling using
326 * de Casteljau's algorithm, see e.g.
327 * http://blog.sklambert.com/finding-the-control-points-of-a-bezier-curve/
328 * of course simplified to symmetric arcs ...
329 */
330 // Interpolate one side of the control polygon
Akronc5b5f742017-05-23 16:04:35 +0200331 var middleY = (((startY + controlY) / 2) + controlY) / 2;
332
Akron6f1302b2017-09-13 12:46:02 +0200333 // Create a boxed label
Akronc9f23022017-09-13 15:43:52 +0200334 g = this._c("g");
335 g.setAttribute("class", "label");
336 this._labelsElement.appendChild(g);
337
338 var that = this;
339 g.addEventListener('mouseenter', function () {
340 that._labelsElement.appendChild(this);
341 });
342
Akron6f1302b2017-09-13 12:46:02 +0200343 var labelE = g.appendChild(this._c("text"));
344 labelE.setAttribute("x", x + middle);
345 labelE.setAttribute("y", middleY + 3);
346 labelE.setAttribute("text-anchor", "middle");
347 var textNode = document.createTextNode(arc.label);
348 labelE.appendChild(textNode);
Akronc5b5f742017-05-23 16:04:35 +0200349
Akron6f1302b2017-09-13 12:46:02 +0200350 var labelBox = labelE.getBBox();
351 var textWidth = labelBox.width; // labelE.getComputedTextLength();
352 var textHeight = labelBox.height; // labelE.getComputedTextLength();
Akron1dc87902017-05-29 16:04:56 +0200353
Akron6f1302b2017-09-13 12:46:02 +0200354 // Add box with padding to left and right
355 var labelR = g.insertBefore(this._c("rect"), labelE);
356 var boxWidth = textWidth + 2 * this.xPadding;
357 labelR.setAttribute("x", x + middle - (boxWidth / 2));
358 labelR.setAttribute("ry", 5);
359 labelR.setAttribute("y", labelBox.y - this.yPadding);
360 labelR.setAttribute("width", boxWidth);
361 labelR.setAttribute("height", textHeight + 2 * this.yPadding);
Akronf5dc5102017-05-16 20:32:57 +0200362 },
363
Akron6f1302b2017-09-13 12:46:02 +0200364 // Get the svg element
Akronf5dc5102017-05-16 20:32:57 +0200365 element : function () {
366 if (this._element !== undefined)
367 return this._element;
368
369 // Create svg
370 var svg = this._c("svg");
Akron3a4a08e2017-05-23 22:34:18 +0200371
372 window.addEventListener("resize", function () {
Akron6f1302b2017-09-13 12:46:02 +0200373 // TODO:
374 // Only if text-size changed!
375 // TODO:
376 // This is currently untested
Akron3a4a08e2017-05-23 22:34:18 +0200377 this.show();
378 }.bind(this));
Akron6f1302b2017-09-13 12:46:02 +0200379
380 // Define marker arrows
Akron3a4a08e2017-05-23 22:34:18 +0200381 var defs = svg.appendChild(this._c("defs"));
382 var marker = defs.appendChild(this._c("marker"));
383 marker.setAttribute("refX", 9);
384 marker.setAttribute("id", "arr");
385 marker.setAttribute("orient", "auto-start-reverse");
386 marker.setAttribute("markerUnits","userSpaceOnUse");
Akron3a4a08e2017-05-23 22:34:18 +0200387 var arrow = this._c("path");
388 arrow.setAttribute("transform", "scale(0.8)");
389 arrow.setAttribute("d", "M 0,-5 0,5 10,0 Z");
390 marker.appendChild(arrow);
391
Akronf5dc5102017-05-16 20:32:57 +0200392 this._element = svg;
393 return this._element;
394 },
395
396 // Add a relation with a start, an end,
Akron6f1302b2017-09-13 12:46:02 +0200397 // a direction value and an optional label text
Akronc5b5f742017-05-23 16:04:35 +0200398 addRel : function (rel) {
399 this._arcs.push(rel);
400 return this;
Akronf5dc5102017-05-16 20:32:57 +0200401 },
402
Akronc5b5f742017-05-23 16:04:35 +0200403
Akron6f1302b2017-09-13 12:46:02 +0200404 // Add a token to the list (this will mostly be a word)
Akronc5b5f742017-05-23 16:04:35 +0200405 addToken : function(token) {
406 this._tokens.push(token);
407 return this;
408 },
409
Akronf5dc5102017-05-16 20:32:57 +0200410 /*
411 * All arcs need to be sorted before shown,
412 * to avoid nesting.
413 */
414 _sortArcs : function () {
415
Akrond67d45b2017-05-18 21:47:38 +0200416 // TODO:
417 // Keep in mind that the arcs may have long anchors!
418 // 1. Iterate over all arcs
419 // 2. Sort all multi
Akron71c87de2017-09-15 14:24:21 +0200420 var anchors = {};
Akrond67d45b2017-05-18 21:47:38 +0200421
Akronf5dc5102017-05-16 20:32:57 +0200422 // 1. Sort by length
423 // 2. Tag all spans with the number of overlaps before
424 // a) Iterate over all spans
425 // b) check the latest preceeding overlapping span (lpos)
426 // -> not found: tag with 0
427 // -> found: Add +1 to the level of the (lpos)
428 // c) If the new tag is smaller than the previous element,
429 // reorder
Akron63ae00b2017-05-16 22:03:36 +0200430
431 // Normalize start and end
432 var sortedArcs = this._arcs.map(function (v) {
Akrond67d45b2017-05-18 21:47:38 +0200433
434 // Check for long anchors
435 if (v.start instanceof Array) {
Akrond67d45b2017-05-18 21:47:38 +0200436
Akron0d04a182017-09-15 14:34:50 +0200437 if (v.start[0] == v.start[1]) {
438 v.start = v.start[0];
439 }
Akrond67d45b2017-05-18 21:47:38 +0200440
Akron0d04a182017-09-15 14:34:50 +0200441 else {
442
443 var middle = Math.ceil(Math.abs(v.start[1] - v.start[0]) / 2) + v.start[0];
444
445 // Calculate signature to avoid multiple anchors
446 var anchorSig = "#" + v.start[0] + "_" + v.start[1];
447 if (v.start[0] > v.start[1]) {
448 anchorSig = "#" + v.start[1] + "_" + v.start[0];
Akron71c87de2017-09-15 14:24:21 +0200449 };
Akron0d04a182017-09-15 14:34:50 +0200450
451 // Check if the anchor already exist
452 var anchor = anchors[anchorSig];
453 if (anchor === undefined) {
454 anchor = {
455 "first": v.start[0],
456 "last" : v.start[1],
457 "length" : v.start[1] - v.start[0]
458 };
459 anchors[anchorSig] = anchor;
460 // anchors.push(v.startAnchor);
461 };
462
463 v.startAnchor = anchor;
464
465 // Add to anchors list
466 v.start = middle;
Akron71c87de2017-09-15 14:24:21 +0200467 };
Akrond67d45b2017-05-18 21:47:38 +0200468 };
469
470 if (v.end instanceof Array) {
Akron71c87de2017-09-15 14:24:21 +0200471
Akron0d04a182017-09-15 14:34:50 +0200472 if (v.end[0] == v.end[1]) {
473 v.end = v.end[0];
474 }
Akrond67d45b2017-05-18 21:47:38 +0200475
Akron0d04a182017-09-15 14:34:50 +0200476 else {
477
478 var middle = Math.abs(v.end[0] - v.end[1]) + v.end[0];
479
480 // Calculate signature to avoid multiple anchors
481 var anchorSig = "#" + v.end[0] + "_" + v.end[1];
482 if (v.end[0] > v.end[1]) {
483 anchorSig = "#" + v.end[1] + "_" + v.end[0];
Akron71c87de2017-09-15 14:24:21 +0200484 };
Akron0d04a182017-09-15 14:34:50 +0200485
486 // Check if the anchor already exist
487 var anchor = anchors[anchorSig];
488 if (anchor === undefined) {
489 anchor = {
490 "first": v.end[0],
491 "last" : v.end[1],
492 "length" : v.end[1] - v.end[0]
493 };
494 anchors[anchorSig] = anchor;
495 // anchors.push(v.startAnchor);
496 };
497
498 v.endAnchor = anchor;
499
500 // Add to anchors list
501 // anchors.push(v.endAnchor);
502 v.end = middle;
Akron71c87de2017-09-15 14:24:21 +0200503 };
Akrond67d45b2017-05-18 21:47:38 +0200504 };
505
Akron2c1d9ff2017-09-15 13:57:33 +0200506 v.first = v.start;
507 v.last = v.end;
508
Akrond67d45b2017-05-18 21:47:38 +0200509 // calculate the arch length
Akron63ae00b2017-05-16 22:03:36 +0200510 if (v.start < v.end) {
Akron63ae00b2017-05-16 22:03:36 +0200511 v.length = v.end - v.start;
512 }
513 else {
Akron2c1d9ff2017-09-15 13:57:33 +0200514 // v.first = v.end;
515 // v.last = v.start;
Akron63ae00b2017-05-16 22:03:36 +0200516 v.length = v.start - v.end;
517 };
Akron2c1d9ff2017-09-15 13:57:33 +0200518
Akron63ae00b2017-05-16 22:03:36 +0200519 return v;
520 });
521
522 // Sort based on length
523 sortedArcs.sort(function (a, b) {
524 if (a.length < b.length)
525 return -1;
526 else
527 return 1;
528 });
529
Akron2c1d9ff2017-09-15 13:57:33 +0200530 // Add sorted arcs and anchors
Akron3a4a08e2017-05-23 22:34:18 +0200531 this._sortedArcs = lengthSort(sortedArcs, false);
Akron71c87de2017-09-15 14:24:21 +0200532
533 // Translate map to array (there is probably a better JS method)
534 var sortedAnchors = [];
535 for (var i in anchors) {
536 sortedAnchors.push(anchors[i]);
537 };
538 this._sortedAnchors = lengthSort(sortedAnchors, true);
Akronf5dc5102017-05-16 20:32:57 +0200539 },
Akron6f1302b2017-09-13 12:46:02 +0200540
Akron0988d882017-11-10 16:13:12 +0100541 /**
542 * Center the viewport of the canvas
543 * TODO:
544 * This is identical to tree
545 */
546 center : function () {
547 if (this._element === undefined)
548 return;
549
550 var treeDiv = this._element.parentNode;
551
552 var cWidth = parseFloat(window.getComputedStyle(this._element).width);
553 var treeWidth = parseFloat(window.getComputedStyle(treeDiv).width);
554 // Reposition:
555 if (cWidth > treeWidth) {
556 var scrollValue = (cWidth - treeWidth) / 2;
557 treeDiv.scrollLeft = scrollValue;
558 };
559 },
560
Akron6f1302b2017-09-13 12:46:02 +0200561
562 // Show the element
Akronf5dc5102017-05-16 20:32:57 +0200563 show : function () {
564 var svg = this._element;
Akron3a4a08e2017-05-23 22:34:18 +0200565 var height = this.maxArc;
566
Akron3a4a08e2017-05-23 22:34:18 +0200567 // Delete old group
568 if (svg.getElementsByTagName("g")[0] !== undefined) {
569 var group = svg.getElementsByTagName("g")[0];
570 svg.removeChild(group);
571 this._tokenElements = [];
572 };
573
574 var g = svg.appendChild(this._c("g"));
Akronf5dc5102017-05-16 20:32:57 +0200575
Akron6f1302b2017-09-13 12:46:02 +0200576 // Draw token list
Akron3a4a08e2017-05-23 22:34:18 +0200577 var text = g.appendChild(this._c("text"));
Akron3d204282017-09-07 18:24:18 +0200578 text.setAttribute('class', 'leaf');
Akron3a4a08e2017-05-23 22:34:18 +0200579 text.setAttribute("text-anchor", "start");
580 text.setAttribute("y", height);
Akronf5dc5102017-05-16 20:32:57 +0200581
Akron6f1302b2017-09-13 12:46:02 +0200582 // Calculate the start position
Akron3a4a08e2017-05-23 22:34:18 +0200583 this._y = height - (this.anchorStart);
584
Akron6f1302b2017-09-13 12:46:02 +0200585 // Introduce some prepending whitespace (yeah - I know ...)
Akron3a4a08e2017-05-23 22:34:18 +0200586 var ws = text.appendChild(this._c("tspan"));
587 ws.appendChild(document.createTextNode('\u00A0'));
588 ws.style.textAnchor = "start";
589
Akronf5dc5102017-05-16 20:32:57 +0200590 var lastRight = 0;
591 for (var node_i in this._tokens) {
592 // Append svg
Akron6c3e5ea2017-11-11 07:16:54 +0100593 // var x = text.appendChild(this._c("text"));
Akronf5dc5102017-05-16 20:32:57 +0200594 var tspan = text.appendChild(this._c("tspan"));
595 tspan.appendChild(document.createTextNode(this._tokens[node_i]));
Akron3a4a08e2017-05-23 22:34:18 +0200596 tspan.setAttribute("text-anchor", "middle");
597
Akronf5dc5102017-05-16 20:32:57 +0200598 this._tokenElements.push(tspan);
599
600 // Add whitespace!
Akron3a4a08e2017-05-23 22:34:18 +0200601 tspan.setAttribute("dx", this.tokenSep);
Akronf5dc5102017-05-16 20:32:57 +0200602 };
603
Akron6f1302b2017-09-13 12:46:02 +0200604 // Get some global position data that may change on resize
Akron6c3e5ea2017-11-11 07:16:54 +0100605 var globalBoundingBox = this._rect(g);
Akron15175132017-09-07 18:12:55 +0200606 this.offsetLeft = globalBoundingBox.left;
607
Akron6f1302b2017-09-13 12:46:02 +0200608 // The group of arcs
Akron3a4a08e2017-05-23 22:34:18 +0200609 var arcs = g.appendChild(this._c("g"));
Akron65d31082017-09-08 16:23:40 +0200610 this._arcsElement = arcs;
Akron3a4a08e2017-05-23 22:34:18 +0200611 arcs.classList.add("arcs");
Akron15175132017-09-07 18:12:55 +0200612
Akronc9f23022017-09-13 15:43:52 +0200613 var labels = g.appendChild(this._c("g"));
614 this._labelsElement = labels;
615 labels.classList.add("labels");
616
Akron15175132017-09-07 18:12:55 +0200617 // Sort arcs if not sorted yet
Akron6f1302b2017-09-13 12:46:02 +0200618 if (this._sortedArcs === undefined)
Akron3a4a08e2017-05-23 22:34:18 +0200619 this._sortArcs();
Akrond67d45b2017-05-18 21:47:38 +0200620
Akron6f1302b2017-09-13 12:46:02 +0200621 // 1. Draw all anchors
Akrond67d45b2017-05-18 21:47:38 +0200622 var i;
623 for (i in this._sortedAnchors) {
Akron65d31082017-09-08 16:23:40 +0200624 this._drawAnchor(this._sortedAnchors[i]);
Akrond67d45b2017-05-18 21:47:38 +0200625 };
Akron15175132017-09-07 18:12:55 +0200626
Akron6f1302b2017-09-13 12:46:02 +0200627 // 2. Draw all arcs
Akrond67d45b2017-05-18 21:47:38 +0200628 for (i in this._sortedArcs) {
Akron65d31082017-09-08 16:23:40 +0200629 this._drawArc(this._sortedArcs[i]);
Akronf5dc5102017-05-16 20:32:57 +0200630 };
Akron3a4a08e2017-05-23 22:34:18 +0200631
Akron6f1302b2017-09-13 12:46:02 +0200632 // Resize the svg with some reasonable margins
Akron6c3e5ea2017-11-11 07:16:54 +0100633 var width = this._rect(text).width;
Akronf3d7d8e2017-05-23 22:52:54 +0200634 svg.setAttribute("width", width + 20);
635 svg.setAttribute("height", height + 20);
Akron3a4a08e2017-05-23 22:34:18 +0200636 svg.setAttribute("class", "relTree");
Akronf5dc5102017-05-16 20:32:57 +0200637 }
Akrond67d45b2017-05-18 21:47:38 +0200638 };
639
Akron6f1302b2017-09-13 12:46:02 +0200640 // Sort relations regarding their span
Akrond67d45b2017-05-18 21:47:38 +0200641 function lengthSort (list, inclusive) {
642
643 /*
644 * The "inclusive" flag allows to
645 * modify the behaviour for inclusivity check,
646 * e.g. if identical start or endpoints mean overlap or not.
647 */
648
649 var stack = [];
650
651 // Iterate over all definitions
652 for (var i = 0; i < list.length; i++) {
653 var current = list[i];
654
655 // Check the stack order
656 var overlaps = 0;
Akrond67d45b2017-05-18 21:47:38 +0200657 for (var j = (stack.length - 1); j >= 0; j--) {
658 var check = stack[j];
659
660 // (a..(b..b)..a)
661 if (current.first <= check.first && current.last >= check.last) {
662 overlaps = check.overlaps + 1;
663 break;
664 }
665
666 // (a..(b..a)..b)
667 else if (current.first <= check.first && current.last >= check.first) {
668
669 if (inclusive || (current.first != check.first && current.last != check.first)) {
670 overlaps = check.overlaps + (current.length == check.length ? 0 : 1);
671 };
672 }
673
674 // (b..(a..b)..a)
675 else if (current.first <= check.last && current.last >= check.last) {
676
677 if (inclusive || (current.first != check.last && current.last != check.last)) {
678 overlaps = check.overlaps + (current.length == check.length ? 0 : 1);
679 };
680 };
681 };
682
683 // Set overlaps
684 current.overlaps = overlaps;
685
686 stack.push(current);
687
688 // Although it is already sorted,
689 // the new item has to be put at the correct place
Akron6f1302b2017-09-13 12:46:02 +0200690 // TODO:
691 // Use something like splice() instead
Akrond67d45b2017-05-18 21:47:38 +0200692 stack.sort(function (a,b) {
693 b.overlaps - a.overlaps
694 });
695 };
696
697 return stack;
698 };
Akronf5dc5102017-05-16 20:32:57 +0200699});