blob: a90fbedf3c3e1088d2a8c5f9b69d45f238290568 [file] [log] [blame]
Akron3ebfd4e2017-11-13 17:56:49 +01001/**
2 * Parse a relational tree and visualize using arcs.
3 *
4 * @author Nils Diewald
5 */
Akrone51eaa32020-11-10 09:35:53 +01006"use strict";
Akron3ebfd4e2017-11-13 17:56:49 +01007
8define([], function () {
Akron3ebfd4e2017-11-13 17:56:49 +01009
Akron0b489ad2018-02-02 16:49:32 +010010 const svgNS = "http://www.w3.org/2000/svg";
11 const _TermRE = new RegExp("^(?:([^\/]+?)\/)?([^:]+?):(.+?)$");
12 const d = document;
Akron3ebfd4e2017-11-13 17:56:49 +010013
14 return {
15 create : function (snippet) {
16 return Object.create(this)._init(snippet);
17 },
18
19 // Initialize the state of the object
20 _init : function (snippet) {
21
Akronff1b1f32020-10-18 11:41:29 +020022 const t = this;
23
Akron3ebfd4e2017-11-13 17:56:49 +010024 // Predefine some values
Akronff1b1f32020-10-18 11:41:29 +020025 t._tokens = [];
26 t._arcs = [];
27 t._tokenElements = [];
28 t._y = 0;
29 t._currentInFocus = undefined;
Akron3ebfd4e2017-11-13 17:56:49 +010030
31 // Some configurations
Akronff1b1f32020-10-18 11:41:29 +020032 t.maxArc = 200; // maximum height of the bezier control point
33 t.overlapDiff = 40; // Difference on overlaps and minimum height for self-refernces
34 t.arcDiff = 15;
35 t.anchorDiff = 8;
36 t.anchorStart = 15;
37 t.tokenSep = 30;
38 t.xPadding = 10;
39 t.yPadding = 5;
Akron3ebfd4e2017-11-13 17:56:49 +010040
41 // No snippet to process
42 if (snippet == undefined || snippet == null)
Akronff1b1f32020-10-18 11:41:29 +020043 return t;
Akron3ebfd4e2017-11-13 17:56:49 +010044
45 // Parse the snippet
Akronff1b1f32020-10-18 11:41:29 +020046 const html = d.createElement("div");
Akron3ebfd4e2017-11-13 17:56:49 +010047 html.innerHTML = snippet;
48
49 // Establish temporary parsing memory
Akronff1b1f32020-10-18 11:41:29 +020050 t.temp = {
Akron3ebfd4e2017-11-13 17:56:49 +010051 target : {}, // Remember the map id => pos
52 edges : [], // Remember edge definitions
53 pos : 0 // Keep track of the current token position
54 };
55
56 // Start parsing from root
Akronff1b1f32020-10-18 11:41:29 +020057 t._parse(0, html.childNodes, undefined);
Akron3ebfd4e2017-11-13 17:56:49 +010058
59 // Establish edge list
Akronff1b1f32020-10-18 11:41:29 +020060 const targetMap = t.temp['target'];
61 const edges = t.temp['edges'];
Akron3ebfd4e2017-11-13 17:56:49 +010062
Akronff1b1f32020-10-18 11:41:29 +020063 let targetID, target, relation;
64
Akron3ebfd4e2017-11-13 17:56:49 +010065 // Iterate over edge lists
66 // TODO:
67 // Support spans for anchors!
Akron678c26f2020-10-09 08:52:50 +020068 edges.forEach(function(edge) {
Akron3ebfd4e2017-11-13 17:56:49 +010069
70 // Check the target identifier
Akronff1b1f32020-10-18 11:41:29 +020071 targetID = edge.targetID;
72 target = targetMap[targetID];
Akron3ebfd4e2017-11-13 17:56:49 +010073
74 if (target != undefined) {
75
76 // Check if the source is a span anchor
77 /*
78 var start = edge.srcStart;
79 if (start !== edge.srcEnd) {
80 start = [start, edge.srcEnd];
81 };
82 */
83
84 // Add relation
Akronff1b1f32020-10-18 11:41:29 +020085 relation = {
Akron3ebfd4e2017-11-13 17:56:49 +010086 start : [edge.srcStart, edge.srcEnd],
87 end : target,
88 direction : 'uni',
89 label : edge.label
90 };
91 // console.log(relation);
92 this.addRel(relation);
93 };
Akronff1b1f32020-10-18 11:41:29 +020094 }, t);
Akron3ebfd4e2017-11-13 17:56:49 +010095
96 // Reset parsing memory
Akronff1b1f32020-10-18 11:41:29 +020097 delete t["temp"];
Akron3ebfd4e2017-11-13 17:56:49 +010098
99 return this;
100 },
101
102 // Parse a node of the tree snippet
103 _parse : function (parent, children, mark) {
104
105 // Iterate over all child nodes
Akronb50964a2020-10-12 11:44:37 +0200106 Array.from(children).forEach(function(c) {
Akron3ebfd4e2017-11-13 17:56:49 +0100107
108 // Element node
109 if (c.nodeType == 1) {
110
Akronff1b1f32020-10-18 11:41:29 +0200111 let xmlid, target, start, end;
Akron3ebfd4e2017-11-13 17:56:49 +0100112
113 // Node is an identifier
114 if (c.hasAttribute('xml:id')) {
115
116 // Remember that pos has this identifier
117 xmlid = c.getAttribute('xml:id');
Akron63581052018-01-31 17:50:59 +0100118 // this.temp['target'][xmlid] =
119 start = this.temp['pos'];
120 end = this.temp['pos'];
Akron3ebfd4e2017-11-13 17:56:49 +0100121 }
122
Akron63581052018-01-31 17:50:59 +0100123 // Node is a link
124 // Stricter: && c.hasAttribute('xlink:show')
Akron3ebfd4e2017-11-13 17:56:49 +0100125 else if (c.hasAttribute('xlink:href')) {
Akron3ebfd4e2017-11-13 17:56:49 +0100126
Akron63581052018-01-31 17:50:59 +0100127 // Node is a continuation
128 if (c.getAttribute('xlink:show') == "other" &&
129 c.hasAttribute('data-action') &&
130 c.getAttribute('data-action') == "join"
131 ) {
132 xmlid = c.getAttribute('xlink:href').replace(/^#/, "");
133 start = this.temp['pos'];
134 end = this.temp['pos'];
Akron3ebfd4e2017-11-13 17:56:49 +0100135
Akron63581052018-01-31 17:50:59 +0100136 // this.temp['target'][xmlid][1] = this.temp['pos'] -1;
137 // console.log("Here");
138 }
139
140 // Node is a relation
141 // Stricter: c.getAttribute('xlink:show') == "none"
142 else {
Akronff1b1f32020-10-18 11:41:29 +0200143 let label;
Akron63581052018-01-31 17:50:59 +0100144
145 // Get target id
146 target = c.getAttribute('xlink:href').replace(/^#/, "");
147
148 if (c.hasAttribute('xlink:title')) {
149 label = this._clean(c.getAttribute('xlink:title'));
150 };
151
152 // Remember the defined edge
Akronff1b1f32020-10-18 11:41:29 +0200153// WRONG!
Akron63581052018-01-31 17:50:59 +0100154 var edge = {
155 label : label,
156 srcStart : this.temp['pos'],
157 targetID : target
158 };
159
160 // TEMP: Hot-fix for root relations
161 if (!label.match(/--$/) && !label.match(/ROOT$/)) {
162 this.temp['edges'].push(edge);
163 };
164
Akron3ebfd4e2017-11-13 17:56:49 +0100165 };
Akron3ebfd4e2017-11-13 17:56:49 +0100166 };
167
168 // Go on with child nodes
169 if (c.hasChildNodes()) {
170 this._parse(0, c.childNodes, mark);
171 };
172
Akron63581052018-01-31 17:50:59 +0100173
174 // The element is a defined anchor
Akron3ebfd4e2017-11-13 17:56:49 +0100175 if (xmlid !== undefined) {
Akron63581052018-01-31 17:50:59 +0100176
177 // this.temp['target'][xmlid][1] = this.temp['pos'] -1;
178
179 // Element already defined
180 if (this.temp['target'][xmlid] !== undefined) {
Akronff1b1f32020-10-18 11:41:29 +0200181 const newtarget = this.temp['target'][xmlid];
Akron63581052018-01-31 17:50:59 +0100182 end = this.temp['pos'] - 1;
183 newtarget[0] = start < newtarget[0] ? start : newtarget[0];
184 newtarget[1] = end > newtarget[1] ? end : newtarget[1];
185 }
186
187 // Element not yet defined
188 else {
189 end = this.temp['pos'] - 1;
190 this.temp['target'][xmlid] = [start, end];
191 };
Akron3ebfd4e2017-11-13 17:56:49 +0100192
193 /*
194 console.log('Target ' + xmlid + ' spans from ' +
195 this.temp['target'][xmlid][0] +
196 ' to ' +
197 this.temp['target'][xmlid][1]
198 );
199 */
200 xmlid = undefined;
201 }
Akron63581052018-01-31 17:50:59 +0100202
203 // Current element describes an arc
Akron3ebfd4e2017-11-13 17:56:49 +0100204 else if (target !== undefined) {
Akron63581052018-01-31 17:50:59 +0100205
206 // TODO: This does not work yet!
Akron3ebfd4e2017-11-13 17:56:49 +0100207 edge["srcEnd"] = this.temp['pos'] -1;
Akron63581052018-01-31 17:50:59 +0100208 // console.log('Here');
Akron3ebfd4e2017-11-13 17:56:49 +0100209
210 /*
211 console.log('Source spans from ' +
212 edge["srcStart"] +
213 ' to ' +
214 edge["srcEnd"]
215 );
216 */
217 target = undefined;
218 };
219 }
220
221 // Text node
222 else if (c.nodeType == 3) {
223
224 // Check, if there is a non-whitespace token
225 if (c.nodeValue !== undefined) {
Akronff1b1f32020-10-18 11:41:29 +0200226
227 const str = c.nodeValue.trim();
228
Akron3ebfd4e2017-11-13 17:56:49 +0100229 if (str !== undefined && str.length > 0) {
230
231 // Add token to token list
232 this.addToken(str);
233
234 // Move token position
235 this.temp['pos']++;
236 };
237 };
Akron678c26f2020-10-09 08:52:50 +0200238 };
239 }, this);
Akron63581052018-01-31 17:50:59 +0100240
241 // Todo: define edges here!
Akron3ebfd4e2017-11-13 17:56:49 +0100242 },
243
244
245 // Remove foundry and layer for labels
246 _clean : function (title) {
247 return title.replace(_TermRE, "$3");
248 },
249
250
251 // Return the number of leaf nodes
252 // (not necessarily part of a relation).
253 // Consecutive nodes that are not part of any
254 // relation are summarized in one node.
255 size : function () {
256 return this._tokens.length;
257 },
258
259
260 // This is a shorthand for SVG element creation
261 _c : function (tag) {
Akron0b489ad2018-02-02 16:49:32 +0100262 return d.createElementNS(svgNS, tag);
Akron3ebfd4e2017-11-13 17:56:49 +0100263 },
264
Akronff1b1f32020-10-18 11:41:29 +0200265
Akron3ebfd4e2017-11-13 17:56:49 +0100266 // Get bounding box - with workaround for text nodes
267 _rect : function (node) {
268 if (node.tagName == "tspan" && !navigator.userAgent.match(/Edge/)) {
Akronff1b1f32020-10-18 11:41:29 +0200269 const range = d.createRange();
Akron3ebfd4e2017-11-13 17:56:49 +0100270 range.selectNode(node);
Akronff1b1f32020-10-18 11:41:29 +0200271 const rect = range.getBoundingClientRect();
Akron3ebfd4e2017-11-13 17:56:49 +0100272 range.detach();
273 return rect;
274 };
275 return node.getBoundingClientRect();
276 },
277
Akronff1b1f32020-10-18 11:41:29 +0200278
Akron3ebfd4e2017-11-13 17:56:49 +0100279 // Returns the center point of the requesting token
280 _tokenPoint : function (node) {
Akronff1b1f32020-10-18 11:41:29 +0200281 const box = this._rect(node);
Akron3ebfd4e2017-11-13 17:56:49 +0100282 return box.left + (box.width / 2);
283 },
284
285
286 // Draws an anchor
287 _drawAnchor : function (anchor) {
288
289 // Calculate the span of the first and last token, the anchor spans
Akronff1b1f32020-10-18 11:41:29 +0200290 const firstBox = this._rect(this._tokenElements[anchor.first]);
291 const lastBox = this._rect(this._tokenElements[anchor.last]);
Akron3ebfd4e2017-11-13 17:56:49 +0100292
Akronff1b1f32020-10-18 11:41:29 +0200293 const y = this._y + (anchor.overlaps * this.anchorDiff) - this.anchorStart;
294 const l = this._c('path');
Akron3ebfd4e2017-11-13 17:56:49 +0100295
Akron3ebfd4e2017-11-13 17:56:49 +0100296 this._arcsElement.appendChild(l);
Akronff1b1f32020-10-18 11:41:29 +0200297
298 const pathStr = "M " +
299 (firstBox.left - this.offsetLeft) +
300 "," +
301 y +
302 " L " +
303 (lastBox.right - this.offsetLeft) +
304 "," + y;
305
Akron3ebfd4e2017-11-13 17:56:49 +0100306 l.setAttribute("d", pathStr);
307 l.setAttribute("class", "anchor");
308 anchor.element = l;
309 anchor.y = y;
310 return l;
311 },
312
313
314 // Create an arc with an optional label
315 // Potentially needs a height parameter for stacks
316 _drawArc : function (arc) {
317
Akronff1b1f32020-10-18 11:41:29 +0200318 const t = this;
319 let startPos, endPos;
320 let startY = this._y;
321 let endY = this._y;
Akron3ebfd4e2017-11-13 17:56:49 +0100322
323 if (arc.startAnchor !== undefined) {
Akronff1b1f32020-10-18 11:41:29 +0200324 startPos = t._tokenPoint(arc.startAnchor.element);
Akron3ebfd4e2017-11-13 17:56:49 +0100325 startY = arc.startAnchor.y;
326 }
327 else {
Akronff1b1f32020-10-18 11:41:29 +0200328 startPos = t._tokenPoint(t._tokenElements[arc.first]);
Akron3ebfd4e2017-11-13 17:56:49 +0100329 };
330
331 if (arc.endAnchor !== undefined) {
Akronff1b1f32020-10-18 11:41:29 +0200332 endPos = t._tokenPoint(arc.endAnchor.element)
Akron3ebfd4e2017-11-13 17:56:49 +0100333 endY = arc.endAnchor.y;
334 }
335 else {
Akronff1b1f32020-10-18 11:41:29 +0200336 endPos = t._tokenPoint(t._tokenElements[arc.last]);
Akron3ebfd4e2017-11-13 17:56:49 +0100337 };
338
Akronff1b1f32020-10-18 11:41:29 +0200339 startPos -= t.offsetLeft;
340 endPos -= t.offsetLeft;
Akron3ebfd4e2017-11-13 17:56:49 +0100341
342 // Special treatment for self-references
343 var overlaps = arc.overlaps;
344 if (startPos == endPos) {
Akronff1b1f32020-10-18 11:41:29 +0200345 startPos -= t.overlapDiff / 3;
346 endPos += t.overlapDiff / 3;
Akron3ebfd4e2017-11-13 17:56:49 +0100347 overlaps += .5;
348 };
349
Akronff1b1f32020-10-18 11:41:29 +0200350 const g = t._c("g");
Akron3ebfd4e2017-11-13 17:56:49 +0100351 g.setAttribute("class", "arc");
Akronff1b1f32020-10-18 11:41:29 +0200352 const p = g.appendChild(t._c("path"));
Akron3ebfd4e2017-11-13 17:56:49 +0100353 p.setAttribute('class', 'edge');
354
355 // Attach the new arc before drawing, so computed values are available
Akronff1b1f32020-10-18 11:41:29 +0200356 t._arcsElement.appendChild(g);
Akron3ebfd4e2017-11-13 17:56:49 +0100357
358 // Create arc
Akronff1b1f32020-10-18 11:41:29 +0200359 let middle = Math.abs(endPos - startPos) / 2;
Akron3ebfd4e2017-11-13 17:56:49 +0100360
361 // TODO:
362 // take the number of tokens into account!
Akronff1b1f32020-10-18 11:41:29 +0200363 let cHeight = t.arcDiff + (overlaps * t.overlapDiff) + (middle / 2);
Akron3ebfd4e2017-11-13 17:56:49 +0100364
365 // Respect the maximum height
Akronff1b1f32020-10-18 11:41:29 +0200366 cHeight = cHeight < t.maxArc ? cHeight : t.maxArc;
Akron3ebfd4e2017-11-13 17:56:49 +0100367
368 var x = Math.min(startPos, endPos);
369
370 //var controlY = (startY + endY - cHeight);
Akronff1b1f32020-10-18 11:41:29 +0200371 let controlY = (endY - cHeight);
Akron3ebfd4e2017-11-13 17:56:49 +0100372
Akronff1b1f32020-10-18 11:41:29 +0200373 const arcE = "M "+ startPos + "," + startY +
Akron3ebfd4e2017-11-13 17:56:49 +0100374 " C " + startPos + "," + controlY +
375 " " + endPos + "," + controlY +
376 " " + endPos + "," + endY;
377
378 p.setAttribute("d", arcE);
379
380 if (arc.direction !== undefined) {
381 p.setAttribute("marker-end", "url(#arr)");
382 if (arc.direction === 'bi') {
383 p.setAttribute("marker-start", "url(#arr)");
384 };
385 };
386
387 if (arc.label === undefined)
388 return g;
389
390 /*
391 * Calculate the top point of the arc for labeling using
392 * de Casteljau's algorithm, see e.g.
393 * http://blog.sklambert.com/finding-the-control-points-of-a-bezier-curve/
394 * of course simplified to symmetric arcs ...
395 */
396 // Interpolate one side of the control polygon
Akronff1b1f32020-10-18 11:41:29 +0200397 let middleY = (((startY + controlY) / 2) + controlY) / 2;
Akron3ebfd4e2017-11-13 17:56:49 +0100398
399 // Create a boxed label
Akronff1b1f32020-10-18 11:41:29 +0200400 const label = this._c("g");
Akron430332b2017-11-20 15:36:51 +0100401 label.setAttribute("class", "label");
Akronff1b1f32020-10-18 11:41:29 +0200402 t._labelsElement.appendChild(label);
Akron430332b2017-11-20 15:36:51 +0100403
404 // Set arc reference
405 label.arcRef = g;
Akron3ebfd4e2017-11-13 17:56:49 +0100406
Akronff1b1f32020-10-18 11:41:29 +0200407 const that = t;
Akron430332b2017-11-20 15:36:51 +0100408 label.addEventListener('mouseenter', function () {
409 that.inFocus(this);
Akron3ebfd4e2017-11-13 17:56:49 +0100410 });
411
Akronff1b1f32020-10-18 11:41:29 +0200412 const labelE = label.appendChild(t._c("text"));
Akron3ebfd4e2017-11-13 17:56:49 +0100413 labelE.setAttribute("x", x + middle);
414 labelE.setAttribute("y", middleY + 3);
415 labelE.setAttribute("text-anchor", "middle");
Akronff1b1f32020-10-18 11:41:29 +0200416 const textNode = d.createTextNode(arc.label);
Akron3ebfd4e2017-11-13 17:56:49 +0100417 labelE.appendChild(textNode);
418
Akronff1b1f32020-10-18 11:41:29 +0200419 const labelBox = labelE.getBBox();
Akronbfe912c2018-07-17 19:30:52 +0200420
Akronff1b1f32020-10-18 11:41:29 +0200421 /*
Akronbfe912c2018-07-17 19:30:52 +0200422 if (!labelBox)
423 console.log("----");
Akronff1b1f32020-10-18 11:41:29 +0200424 */
Akronbfe912c2018-07-17 19:30:52 +0200425
Akronff1b1f32020-10-18 11:41:29 +0200426 const textWidth = labelBox.width; // labelE.getComputedTextLength();
427 const textHeight = labelBox.height; // labelE.getComputedTextLength();
Akron3ebfd4e2017-11-13 17:56:49 +0100428
429 // Add box with padding to left and right
Akronff1b1f32020-10-18 11:41:29 +0200430 const labelR = label.insertBefore(t._c("rect"), labelE);
431 const boxWidth = textWidth + 2 * t.xPadding;
Akron3ebfd4e2017-11-13 17:56:49 +0100432 labelR.setAttribute("x", x + middle - (boxWidth / 2));
433 labelR.setAttribute("ry", 5);
Akronff1b1f32020-10-18 11:41:29 +0200434 labelR.setAttribute("y", labelBox.y - t.yPadding);
Akron3ebfd4e2017-11-13 17:56:49 +0100435 labelR.setAttribute("width", boxWidth);
Akronff1b1f32020-10-18 11:41:29 +0200436 labelR.setAttribute("height", textHeight + 2 * t.yPadding);
Akron3ebfd4e2017-11-13 17:56:49 +0100437 },
438
Akronff1b1f32020-10-18 11:41:29 +0200439
440 /**
441 * Get the svg element
442 */
Akron3ebfd4e2017-11-13 17:56:49 +0100443 element : function () {
Akron24aa0052020-11-10 11:00:34 +0100444 if (this._el !== undefined)
445 return this._el;
Akron3ebfd4e2017-11-13 17:56:49 +0100446
447 // Create svg
Akronff1b1f32020-10-18 11:41:29 +0200448 const svg = this._c("svg");
Akron3ebfd4e2017-11-13 17:56:49 +0100449
450 window.addEventListener("resize", function () {
451 // TODO:
452 // Only if text-size changed!
453 // TODO:
454 // This is currently untested
455 this.show();
456 }.bind(this));
457
458 // Define marker arrows
Akronff1b1f32020-10-18 11:41:29 +0200459 const defs = svg.appendChild(this._c("defs"));
460 const marker = defs.appendChild(this._c("marker"));
Akron3ebfd4e2017-11-13 17:56:49 +0100461 marker.setAttribute("refX", 9);
462 marker.setAttribute("id", "arr");
463 marker.setAttribute("orient", "auto-start-reverse");
464 marker.setAttribute("markerUnits","userSpaceOnUse");
Akronff1b1f32020-10-18 11:41:29 +0200465
466 const arrow = this._c("path");
Akron3ebfd4e2017-11-13 17:56:49 +0100467 arrow.setAttribute("transform", "scale(0.8)");
468 arrow.setAttribute("d", "M 0,-5 0,5 10,0 Z");
469 marker.appendChild(arrow);
470
Akron24aa0052020-11-10 11:00:34 +0100471 this._el = svg;
472 return this._el;
Akron3ebfd4e2017-11-13 17:56:49 +0100473 },
474
Akronff1b1f32020-10-18 11:41:29 +0200475
Akron3ebfd4e2017-11-13 17:56:49 +0100476 // Add a relation with a start, an end,
477 // a direction value and an optional label text
478 addRel : function (rel) {
479 this._arcs.push(rel);
480 return this;
481 },
482
483
484 // Add a token to the list (this will mostly be a word)
485 addToken : function(token) {
486 this._tokens.push(token);
487 return this;
488 },
Akron430332b2017-11-20 15:36:51 +0100489
490
491 // Move label and arc in focus
492 inFocus : function (element) {
Akronff1b1f32020-10-18 11:41:29 +0200493 let cif;
Akron430332b2017-11-20 15:36:51 +0100494
495 if (this._currentInFocus) {
496
497 // Already in focus
498 if (this._currentInFocus === element)
499 return;
500
501 cif = this._currentInFocus;
502 cif.classList.remove('infocus');
503 cif.arcRef.classList.remove('infocus');
504 };
505
506 cif = this._currentInFocus = element;
507 this._labelsElement.appendChild(cif);
508 this._arcsElement.appendChild(cif.arcRef);
509 cif.classList.add('infocus');
510 cif.arcRef.classList.add('infocus');
511 },
Akron3ebfd4e2017-11-13 17:56:49 +0100512
Akronff1b1f32020-10-18 11:41:29 +0200513
Akron3ebfd4e2017-11-13 17:56:49 +0100514 /*
515 * All arcs need to be sorted before shown,
516 * to avoid nesting.
517 */
518 _sortArcs : function () {
519
520 // TODO:
521 // Keep in mind that the arcs may have long anchors!
522 // 1. Iterate over all arcs
523 // 2. Sort all multi
Akronff1b1f32020-10-18 11:41:29 +0200524 let anchors = {};
Akron3ebfd4e2017-11-13 17:56:49 +0100525
526 // 1. Sort by length
527 // 2. Tag all spans with the number of overlaps before
528 // a) Iterate over all spans
529 // b) check the latest preceeding overlapping span (lpos)
530 // -> not found: tag with 0
531 // -> found: Add +1 to the level of the (lpos)
532 // c) If the new tag is smaller than the previous element,
533 // reorder
534
535 // Normalize start and end
Akronff1b1f32020-10-18 11:41:29 +0200536 const sortedArcs = this._arcs.map(function (v) {
Akron3ebfd4e2017-11-13 17:56:49 +0100537
538 // Check for long anchors
539 if (v.start instanceof Array) {
540
541 if (v.start[0] == v.start[1]) {
542 v.start = v.start[0];
543 }
544
545 else {
546
Akronff1b1f32020-10-18 11:41:29 +0200547 const middle = Math.ceil(Math.abs(v.start[1] - v.start[0]) / 2) + v.start[0];
Akron3ebfd4e2017-11-13 17:56:49 +0100548
549 // Calculate signature to avoid multiple anchors
Akronff1b1f32020-10-18 11:41:29 +0200550 let anchorSig = "#" + v.start[0] + "_" + v.start[1];
551
552 // Reverse signature
Akron3ebfd4e2017-11-13 17:56:49 +0100553 if (v.start[0] > v.start[1]) {
554 anchorSig = "#" + v.start[1] + "_" + v.start[0];
555 };
556
557 // Check if the anchor already exist
Akronff1b1f32020-10-18 11:41:29 +0200558 let anchor = anchors[anchorSig];
Akron3ebfd4e2017-11-13 17:56:49 +0100559 if (anchor === undefined) {
560 anchor = {
561 "first": v.start[0],
562 "last" : v.start[1],
563 "length" : v.start[1] - v.start[0]
564 };
565 anchors[anchorSig] = anchor;
Akron3ebfd4e2017-11-13 17:56:49 +0100566 };
567
568 v.startAnchor = anchor;
569
570 // Add to anchors list
571 v.start = middle;
572 };
573 };
574
575 if (v.end instanceof Array) {
576
577 if (v.end[0] == v.end[1]) {
578 v.end = v.end[0];
579 }
580
581 else {
582
Akronff1b1f32020-10-18 11:41:29 +0200583 const middle = Math.abs(v.end[0] - v.end[1]) + v.end[0];
Akron3ebfd4e2017-11-13 17:56:49 +0100584
585 // Calculate signature to avoid multiple anchors
Akronff1b1f32020-10-18 11:41:29 +0200586 let anchorSig = "#" + v.end[0] + "_" + v.end[1];
587
588 // Reverse signature
Akron3ebfd4e2017-11-13 17:56:49 +0100589 if (v.end[0] > v.end[1]) {
590 anchorSig = "#" + v.end[1] + "_" + v.end[0];
591 };
592
593 // Check if the anchor already exist
594 var anchor = anchors[anchorSig];
595 if (anchor === undefined) {
596 anchor = {
597 "first": v.end[0],
598 "last" : v.end[1],
599 "length" : v.end[1] - v.end[0]
600 };
601 anchors[anchorSig] = anchor;
Akron3ebfd4e2017-11-13 17:56:49 +0100602 };
603
604 v.endAnchor = anchor;
605
606 // Add to anchors list
Akron3ebfd4e2017-11-13 17:56:49 +0100607 v.end = middle;
608 };
609 };
610
611 v.first = v.start;
Akronff1b1f32020-10-18 11:41:29 +0200612 v.last = v.end;
Akron3ebfd4e2017-11-13 17:56:49 +0100613
614 // calculate the arch length
615 if (v.start < v.end) {
616 v.length = v.end - v.start;
617 }
Akronff1b1f32020-10-18 11:41:29 +0200618
Akron3ebfd4e2017-11-13 17:56:49 +0100619 else {
Akron3ebfd4e2017-11-13 17:56:49 +0100620 v.length = v.start - v.end;
621 };
622
623 return v;
624 });
625
626 // Sort based on length
627 sortedArcs.sort(function (a, b) {
628 if (a.length < b.length)
629 return -1;
630 else
631 return 1;
632 });
633
634 // Add sorted arcs and anchors
635 this._sortedArcs = lengthSort(sortedArcs, false);
Akron0a785d42020-10-12 17:21:46 +0200636 this._sortedAnchors = lengthSort(Object.values(anchors), true);
Akron3ebfd4e2017-11-13 17:56:49 +0100637 },
638
Akronff1b1f32020-10-18 11:41:29 +0200639
Akron3ebfd4e2017-11-13 17:56:49 +0100640 /**
641 * Center the viewport of the canvas
642 * TODO:
Akronff1b1f32020-10-18 11:41:29 +0200643 * This is identical to treehierarchy
Akron3ebfd4e2017-11-13 17:56:49 +0100644 */
645 center : function () {
Akron24aa0052020-11-10 11:00:34 +0100646 if (this._el === undefined)
Akron3ebfd4e2017-11-13 17:56:49 +0100647 return;
648
Akron24aa0052020-11-10 11:00:34 +0100649 const treeDiv = this._el.parentNode;
Akron3ebfd4e2017-11-13 17:56:49 +0100650
Akron24aa0052020-11-10 11:00:34 +0100651 const cWidth = parseFloat(window.getComputedStyle(this._el).width);
Akronff1b1f32020-10-18 11:41:29 +0200652 const treeWidth = parseFloat(window.getComputedStyle(treeDiv).width);
Akron3ebfd4e2017-11-13 17:56:49 +0100653 // Reposition:
654 if (cWidth > treeWidth) {
655 var scrollValue = (cWidth - treeWidth) / 2;
656 treeDiv.scrollLeft = scrollValue;
657 };
658 },
659
660
661 // Show the element
662 show : function () {
Akronff1b1f32020-10-18 11:41:29 +0200663 const t = this;
Akron24aa0052020-11-10 11:00:34 +0100664 const svg = this._el;
Akronff1b1f32020-10-18 11:41:29 +0200665 const height = this.maxArc;
Akron3ebfd4e2017-11-13 17:56:49 +0100666
667 // Delete old group
668 if (svg.getElementsByTagName("g")[0] !== undefined) {
Akronff1b1f32020-10-18 11:41:29 +0200669 svg.removeChild(
670 svg.getElementsByTagName("g")[0]
671 );
672 t._tokenElements = [];
Akron3ebfd4e2017-11-13 17:56:49 +0100673 };
674
Akronff1b1f32020-10-18 11:41:29 +0200675 const g = svg.appendChild(t._c("g"));
Akron3ebfd4e2017-11-13 17:56:49 +0100676
677 // Draw token list
Akronff1b1f32020-10-18 11:41:29 +0200678 const text = g.appendChild(t._c("text"));
Akron3ebfd4e2017-11-13 17:56:49 +0100679 text.setAttribute('class', 'leaf');
680 text.setAttribute("text-anchor", "start");
681 text.setAttribute("y", height);
682
683 // Calculate the start position
Akronff1b1f32020-10-18 11:41:29 +0200684 t._y = height - (t.anchorStart);
Akron3ebfd4e2017-11-13 17:56:49 +0100685
686 // Introduce some prepending whitespace (yeah - I know ...)
Akronff1b1f32020-10-18 11:41:29 +0200687 const ws = text.appendChild(t._c("tspan"));
Akron0b489ad2018-02-02 16:49:32 +0100688 ws.appendChild(d.createTextNode('\u00A0'));
Akron3ebfd4e2017-11-13 17:56:49 +0100689 ws.style.textAnchor = "start";
690
Akronff1b1f32020-10-18 11:41:29 +0200691 t._tokens.forEach(function(node_i) {
Akron3ebfd4e2017-11-13 17:56:49 +0100692 // Append svg
693 // var x = text.appendChild(this._c("text"));
Akronff1b1f32020-10-18 11:41:29 +0200694 const tspan = text.appendChild(this._c("tspan"));
Akron678c26f2020-10-09 08:52:50 +0200695 tspan.appendChild(d.createTextNode(node_i));
Akron3ebfd4e2017-11-13 17:56:49 +0100696 tspan.setAttribute("text-anchor", "middle");
697
698 this._tokenElements.push(tspan);
699
700 // Add whitespace!
701 tspan.setAttribute("dx", this.tokenSep);
Akronff1b1f32020-10-18 11:41:29 +0200702 }, t);
Akron3ebfd4e2017-11-13 17:56:49 +0100703
704 // Get some global position data that may change on resize
Akronff1b1f32020-10-18 11:41:29 +0200705 t.offsetLeft = t._rect(g).left;
Akron3ebfd4e2017-11-13 17:56:49 +0100706
707 // The group of arcs
Akronff1b1f32020-10-18 11:41:29 +0200708 const arcs = g.appendChild(t._c("g"));
709 t._arcsElement = arcs;
Akron3ebfd4e2017-11-13 17:56:49 +0100710 arcs.classList.add("arcs");
711
Akronff1b1f32020-10-18 11:41:29 +0200712 const labels = g.appendChild(t._c("g"));
713 t._labelsElement = labels;
Akron3ebfd4e2017-11-13 17:56:49 +0100714 labels.classList.add("labels");
715
716 // Sort arcs if not sorted yet
Akronff1b1f32020-10-18 11:41:29 +0200717 if (t._sortedArcs === undefined)
718 t._sortArcs();
Akron3ebfd4e2017-11-13 17:56:49 +0100719
720 // 1. Draw all anchors
Akronff1b1f32020-10-18 11:41:29 +0200721 t._sortedAnchors.forEach(
722 i => t._drawAnchor(i)
Akron678c26f2020-10-09 08:52:50 +0200723 );
Akron3ebfd4e2017-11-13 17:56:49 +0100724
725 // 2. Draw all arcs
Akronff1b1f32020-10-18 11:41:29 +0200726 t._sortedArcs.forEach(
727 i => t._drawArc(i)
Akron678c26f2020-10-09 08:52:50 +0200728 );
Akron3ebfd4e2017-11-13 17:56:49 +0100729
730 // Resize the svg with some reasonable margins
Akronff1b1f32020-10-18 11:41:29 +0200731 svg.setAttribute("width", t._rect(text).width + 20);
Akron3ebfd4e2017-11-13 17:56:49 +0100732 svg.setAttribute("height", height + 20);
733 svg.setAttribute("class", "relTree");
734 }
735 };
736
737 // Sort relations regarding their span
738 function lengthSort (list, inclusive) {
739
740 /*
741 * The "inclusive" flag allows to
742 * modify the behaviour for inclusivity check,
743 * e.g. if identical start or endpoints mean overlap or not.
744 */
745
Akronff1b1f32020-10-18 11:41:29 +0200746 let stack = [];
Akron3ebfd4e2017-11-13 17:56:49 +0100747
748 // Iterate over all definitions
Akronb50964a2020-10-12 11:44:37 +0200749 list.forEach(function(current) {
Akron3ebfd4e2017-11-13 17:56:49 +0100750
751 // Check the stack order
Akronff1b1f32020-10-18 11:41:29 +0200752 let overlaps = 0;
753 let check;
754 for (let j = (stack.length - 1); j >= 0; j--) {
755 check = stack[j];
Akron3ebfd4e2017-11-13 17:56:49 +0100756
757 // (a..(b..b)..a)
758 if (current.first <= check.first && current.last >= check.last) {
759 overlaps = check.overlaps + 1;
760 break;
761 }
762
763 // (a..(b..a)..b)
764 else if (current.first <= check.first && current.last >= check.first) {
765
766 if (inclusive || (current.first != check.first && current.last != check.first)) {
767 overlaps = check.overlaps + (current.length == check.length ? 0 : 1);
768 };
769 }
770
771 // (b..(a..b)..a)
772 else if (current.first <= check.last && current.last >= check.last) {
773
774 if (inclusive || (current.first != check.last && current.last != check.last)) {
775 overlaps = check.overlaps + (current.length == check.length ? 0 : 1);
776 };
777 };
778 };
779
780 // Set overlaps
781 current.overlaps = overlaps;
782
783 stack.push(current);
784
785 // Although it is already sorted,
786 // the new item has to be put at the correct place
787 // TODO:
788 // Use something like splice() instead
789 stack.sort(function (a,b) {
790 b.overlaps - a.overlaps
791 });
Akronb50964a2020-10-12 11:44:37 +0200792 });
Akron3ebfd4e2017-11-13 17:56:49 +0100793
794 return stack;
795 };
796});