blob: badbacfbfe40fc0a6831a0065580098314c991a9 [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 */
6
7define([], function () {
8 "use strict";
9
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
22 // Predefine some values
23 this._tokens = [];
24 this._arcs = [];
25 this._tokenElements = [];
26 this._y = 0;
Akron430332b2017-11-20 15:36:51 +010027 this._currentInFocus = undefined;
Akron3ebfd4e2017-11-13 17:56:49 +010028
29 // Some configurations
30 this.maxArc = 200; // maximum height of the bezier control point
31 this.overlapDiff = 40; // Difference on overlaps and minimum height for self-refernces
32 this.arcDiff = 15;
33 this.anchorDiff = 8;
34 this.anchorStart = 15;
35 this.tokenSep = 30;
36 this.xPadding = 10;
37 this.yPadding = 5;
38
39 // No snippet to process
40 if (snippet == undefined || snippet == null)
41 return this;
42
43 // Parse the snippet
Akron0b489ad2018-02-02 16:49:32 +010044 var html = d.createElement("div");
Akron3ebfd4e2017-11-13 17:56:49 +010045 html.innerHTML = snippet;
46
47 // Establish temporary parsing memory
48 this.temp = {
49 target : {}, // Remember the map id => pos
50 edges : [], // Remember edge definitions
51 pos : 0 // Keep track of the current token position
52 };
53
54 // Start parsing from root
55 this._parse(0, html.childNodes, undefined);
56
57 // Establish edge list
58 var targetMap = this.temp['target'];
59 var edges = this.temp['edges'];
60
61 // Iterate over edge lists
62 // TODO:
63 // Support spans for anchors!
64 for (var i in edges) {
65 var edge = edges[i];
66
67 // Check the target identifier
68 var targetID = edge.targetID;
69 var target = targetMap[targetID];
70
71 if (target != undefined) {
72
73 // Check if the source is a span anchor
74 /*
75 var start = edge.srcStart;
76 if (start !== edge.srcEnd) {
77 start = [start, edge.srcEnd];
78 };
79 */
80
81 // Add relation
82 var relation = {
83 start : [edge.srcStart, edge.srcEnd],
84 end : target,
85 direction : 'uni',
86 label : edge.label
87 };
88 // console.log(relation);
89 this.addRel(relation);
90 };
91 };
92
93 // Reset parsing memory
94 this.temp = {};
95
96 return this;
97 },
98
99 // Parse a node of the tree snippet
100 _parse : function (parent, children, mark) {
101
102 // Iterate over all child nodes
103 for (var i in children) {
104 var c = children[i];
105
106 // Element node
107 if (c.nodeType == 1) {
108
Akron63581052018-01-31 17:50:59 +0100109 var xmlid, target, start, end;
Akron3ebfd4e2017-11-13 17:56:49 +0100110
111 // Node is an identifier
112 if (c.hasAttribute('xml:id')) {
113
114 // Remember that pos has this identifier
115 xmlid = c.getAttribute('xml:id');
Akron63581052018-01-31 17:50:59 +0100116 // this.temp['target'][xmlid] =
117 start = this.temp['pos'];
118 end = this.temp['pos'];
Akron3ebfd4e2017-11-13 17:56:49 +0100119 }
120
Akron63581052018-01-31 17:50:59 +0100121 // Node is a link
122 // Stricter: && c.hasAttribute('xlink:show')
Akron3ebfd4e2017-11-13 17:56:49 +0100123 else if (c.hasAttribute('xlink:href')) {
Akron3ebfd4e2017-11-13 17:56:49 +0100124
Akron63581052018-01-31 17:50:59 +0100125 // Node is a continuation
126 if (c.getAttribute('xlink:show') == "other" &&
127 c.hasAttribute('data-action') &&
128 c.getAttribute('data-action') == "join"
129 ) {
130 xmlid = c.getAttribute('xlink:href').replace(/^#/, "");
131 start = this.temp['pos'];
132 end = this.temp['pos'];
Akron3ebfd4e2017-11-13 17:56:49 +0100133
Akron63581052018-01-31 17:50:59 +0100134 // this.temp['target'][xmlid][1] = this.temp['pos'] -1;
135 // console.log("Here");
136 }
137
138 // Node is a relation
139 // Stricter: c.getAttribute('xlink:show') == "none"
140 else {
141 var label;
142
143 // Get target id
144 target = c.getAttribute('xlink:href').replace(/^#/, "");
145
146 if (c.hasAttribute('xlink:title')) {
147 label = this._clean(c.getAttribute('xlink:title'));
148 };
149
150 // Remember the defined edge
151 var edge = {
152 label : label,
153 srcStart : this.temp['pos'],
154 targetID : target
155 };
156
157 // TEMP: Hot-fix for root relations
158 if (!label.match(/--$/) && !label.match(/ROOT$/)) {
159 this.temp['edges'].push(edge);
160 };
161
Akron3ebfd4e2017-11-13 17:56:49 +0100162 };
Akron3ebfd4e2017-11-13 17:56:49 +0100163 };
164
165 // Go on with child nodes
166 if (c.hasChildNodes()) {
167 this._parse(0, c.childNodes, mark);
168 };
169
Akron63581052018-01-31 17:50:59 +0100170
171 // The element is a defined anchor
Akron3ebfd4e2017-11-13 17:56:49 +0100172 if (xmlid !== undefined) {
Akron63581052018-01-31 17:50:59 +0100173
174 // this.temp['target'][xmlid][1] = this.temp['pos'] -1;
175
176 // Element already defined
177 if (this.temp['target'][xmlid] !== undefined) {
178 var newtarget = this.temp['target'][xmlid];
179 end = this.temp['pos'] - 1;
180 newtarget[0] = start < newtarget[0] ? start : newtarget[0];
181 newtarget[1] = end > newtarget[1] ? end : newtarget[1];
182 }
183
184 // Element not yet defined
185 else {
186 end = this.temp['pos'] - 1;
187 this.temp['target'][xmlid] = [start, end];
188 };
Akron3ebfd4e2017-11-13 17:56:49 +0100189
190 /*
191 console.log('Target ' + xmlid + ' spans from ' +
192 this.temp['target'][xmlid][0] +
193 ' to ' +
194 this.temp['target'][xmlid][1]
195 );
196 */
197 xmlid = undefined;
198 }
Akron63581052018-01-31 17:50:59 +0100199
200 // Current element describes an arc
Akron3ebfd4e2017-11-13 17:56:49 +0100201 else if (target !== undefined) {
Akron63581052018-01-31 17:50:59 +0100202
203 // TODO: This does not work yet!
Akron3ebfd4e2017-11-13 17:56:49 +0100204 edge["srcEnd"] = this.temp['pos'] -1;
Akron63581052018-01-31 17:50:59 +0100205 // console.log('Here');
Akron3ebfd4e2017-11-13 17:56:49 +0100206
207 /*
208 console.log('Source spans from ' +
209 edge["srcStart"] +
210 ' to ' +
211 edge["srcEnd"]
212 );
213 */
214 target = undefined;
215 };
216 }
217
218 // Text node
219 else if (c.nodeType == 3) {
220
221 // Check, if there is a non-whitespace token
222 if (c.nodeValue !== undefined) {
223 var str = c.nodeValue.trim();
224 if (str !== undefined && str.length > 0) {
225
226 // Add token to token list
227 this.addToken(str);
228
229 // Move token position
230 this.temp['pos']++;
231 };
232 };
233 }
234 };
Akron63581052018-01-31 17:50:59 +0100235
236 // Todo: define edges here!
Akron3ebfd4e2017-11-13 17:56:49 +0100237 },
238
239
240 // Remove foundry and layer for labels
241 _clean : function (title) {
242 return title.replace(_TermRE, "$3");
243 },
244
245
246 // Return the number of leaf nodes
247 // (not necessarily part of a relation).
248 // Consecutive nodes that are not part of any
249 // relation are summarized in one node.
250 size : function () {
251 return this._tokens.length;
252 },
253
254
255 // This is a shorthand for SVG element creation
256 _c : function (tag) {
Akron0b489ad2018-02-02 16:49:32 +0100257 return d.createElementNS(svgNS, tag);
Akron3ebfd4e2017-11-13 17:56:49 +0100258 },
259
260 // Get bounding box - with workaround for text nodes
261 _rect : function (node) {
262 if (node.tagName == "tspan" && !navigator.userAgent.match(/Edge/)) {
Akron0b489ad2018-02-02 16:49:32 +0100263 var range = d.createRange();
Akron3ebfd4e2017-11-13 17:56:49 +0100264 range.selectNode(node);
265 var rect = range.getBoundingClientRect();
266 range.detach();
267 return rect;
268 };
269 return node.getBoundingClientRect();
270 },
271
272 // Returns the center point of the requesting token
273 _tokenPoint : function (node) {
274 var box = this._rect(node);
275 return box.left + (box.width / 2);
276 },
277
278
279 // Draws an anchor
280 _drawAnchor : function (anchor) {
281
282 // Calculate the span of the first and last token, the anchor spans
283 var firstBox = this._rect(this._tokenElements[anchor.first]);
284 var lastBox = this._rect(this._tokenElements[anchor.last]);
285
286 var startPos = firstBox.left - this.offsetLeft;
287 var endPos = lastBox.right - this.offsetLeft;
288
289 var y = this._y + (anchor.overlaps * this.anchorDiff) - this.anchorStart;
290
291 var l = this._c('path');
292 this._arcsElement.appendChild(l);
293 var pathStr = "M " + startPos + "," + y + " L " + endPos + "," + y;
294 l.setAttribute("d", pathStr);
295 l.setAttribute("class", "anchor");
296 anchor.element = l;
297 anchor.y = y;
298 return l;
299 },
300
301
302 // Create an arc with an optional label
303 // Potentially needs a height parameter for stacks
304 _drawArc : function (arc) {
305
306 var startPos, endPos;
307 var startY = this._y;
308 var endY = this._y;
309
310 if (arc.startAnchor !== undefined) {
311 startPos = this._tokenPoint(arc.startAnchor.element);
312 startY = arc.startAnchor.y;
313 }
314 else {
315 startPos = this._tokenPoint(this._tokenElements[arc.first]);
316 };
317
318 if (arc.endAnchor !== undefined) {
319 endPos = this._tokenPoint(arc.endAnchor.element)
320 endY = arc.endAnchor.y;
321 }
322 else {
323 endPos = this._tokenPoint(this._tokenElements[arc.last]);
324 };
325
Akron3ebfd4e2017-11-13 17:56:49 +0100326 startPos -= this.offsetLeft;
327 endPos -= this.offsetLeft;
328
329 // Special treatment for self-references
330 var overlaps = arc.overlaps;
331 if (startPos == endPos) {
332 startPos -= this.overlapDiff / 3;
333 endPos += this.overlapDiff / 3;
334 overlaps += .5;
335 };
336
337 var g = this._c("g");
338 g.setAttribute("class", "arc");
339 var p = g.appendChild(this._c("path"));
340 p.setAttribute('class', 'edge');
341
342 // Attach the new arc before drawing, so computed values are available
343 this._arcsElement.appendChild(g);
344
345 // Create arc
346 var middle = Math.abs(endPos - startPos) / 2;
347
348 // TODO:
349 // take the number of tokens into account!
350 var cHeight = this.arcDiff + (overlaps * this.overlapDiff) + (middle / 2);
351
352 // Respect the maximum height
353 cHeight = cHeight < this.maxArc ? cHeight : this.maxArc;
354
355 var x = Math.min(startPos, endPos);
356
357 //var controlY = (startY + endY - cHeight);
358 var controlY = (endY - cHeight);
359
360 var arcE = "M "+ startPos + "," + startY +
361 " C " + startPos + "," + controlY +
362 " " + endPos + "," + controlY +
363 " " + endPos + "," + endY;
364
365 p.setAttribute("d", arcE);
366
367 if (arc.direction !== undefined) {
368 p.setAttribute("marker-end", "url(#arr)");
369 if (arc.direction === 'bi') {
370 p.setAttribute("marker-start", "url(#arr)");
371 };
372 };
373
374 if (arc.label === undefined)
375 return g;
376
377 /*
378 * Calculate the top point of the arc for labeling using
379 * de Casteljau's algorithm, see e.g.
380 * http://blog.sklambert.com/finding-the-control-points-of-a-bezier-curve/
381 * of course simplified to symmetric arcs ...
382 */
383 // Interpolate one side of the control polygon
384 var middleY = (((startY + controlY) / 2) + controlY) / 2;
385
386 // Create a boxed label
Akron430332b2017-11-20 15:36:51 +0100387 var label = this._c("g");
388 label.setAttribute("class", "label");
389 this._labelsElement.appendChild(label);
390
391 // Set arc reference
392 label.arcRef = g;
Akron3ebfd4e2017-11-13 17:56:49 +0100393
394 var that = this;
Akron430332b2017-11-20 15:36:51 +0100395 label.addEventListener('mouseenter', function () {
396 that.inFocus(this);
Akron3ebfd4e2017-11-13 17:56:49 +0100397 });
398
Akron430332b2017-11-20 15:36:51 +0100399 var labelE = label.appendChild(this._c("text"));
Akron3ebfd4e2017-11-13 17:56:49 +0100400 labelE.setAttribute("x", x + middle);
401 labelE.setAttribute("y", middleY + 3);
402 labelE.setAttribute("text-anchor", "middle");
Akron0b489ad2018-02-02 16:49:32 +0100403 var textNode = d.createTextNode(arc.label);
Akron3ebfd4e2017-11-13 17:56:49 +0100404 labelE.appendChild(textNode);
405
406 var labelBox = labelE.getBBox();
407 var textWidth = labelBox.width; // labelE.getComputedTextLength();
408 var textHeight = labelBox.height; // labelE.getComputedTextLength();
409
410 // Add box with padding to left and right
Akron430332b2017-11-20 15:36:51 +0100411 var labelR = label.insertBefore(this._c("rect"), labelE);
Akron3ebfd4e2017-11-13 17:56:49 +0100412 var boxWidth = textWidth + 2 * this.xPadding;
413 labelR.setAttribute("x", x + middle - (boxWidth / 2));
414 labelR.setAttribute("ry", 5);
415 labelR.setAttribute("y", labelBox.y - this.yPadding);
416 labelR.setAttribute("width", boxWidth);
417 labelR.setAttribute("height", textHeight + 2 * this.yPadding);
418 },
419
420 // Get the svg element
421 element : function () {
422 if (this._element !== undefined)
423 return this._element;
424
425 // Create svg
426 var svg = this._c("svg");
427
428 window.addEventListener("resize", function () {
429 // TODO:
430 // Only if text-size changed!
431 // TODO:
432 // This is currently untested
433 this.show();
434 }.bind(this));
435
436 // Define marker arrows
437 var defs = svg.appendChild(this._c("defs"));
438 var marker = defs.appendChild(this._c("marker"));
439 marker.setAttribute("refX", 9);
440 marker.setAttribute("id", "arr");
441 marker.setAttribute("orient", "auto-start-reverse");
442 marker.setAttribute("markerUnits","userSpaceOnUse");
443 var arrow = this._c("path");
444 arrow.setAttribute("transform", "scale(0.8)");
445 arrow.setAttribute("d", "M 0,-5 0,5 10,0 Z");
446 marker.appendChild(arrow);
447
448 this._element = svg;
449 return this._element;
450 },
451
452 // Add a relation with a start, an end,
453 // a direction value and an optional label text
454 addRel : function (rel) {
455 this._arcs.push(rel);
456 return this;
457 },
458
459
460 // Add a token to the list (this will mostly be a word)
461 addToken : function(token) {
462 this._tokens.push(token);
463 return this;
464 },
Akron430332b2017-11-20 15:36:51 +0100465
466
467 // Move label and arc in focus
468 inFocus : function (element) {
469 var cif;
470
471 if (this._currentInFocus) {
472
473 // Already in focus
474 if (this._currentInFocus === element)
475 return;
476
477 cif = this._currentInFocus;
478 cif.classList.remove('infocus');
479 cif.arcRef.classList.remove('infocus');
480 };
481
482 cif = this._currentInFocus = element;
483 this._labelsElement.appendChild(cif);
484 this._arcsElement.appendChild(cif.arcRef);
485 cif.classList.add('infocus');
486 cif.arcRef.classList.add('infocus');
487 },
Akron3ebfd4e2017-11-13 17:56:49 +0100488
489 /*
490 * All arcs need to be sorted before shown,
491 * to avoid nesting.
492 */
493 _sortArcs : function () {
494
495 // TODO:
496 // Keep in mind that the arcs may have long anchors!
497 // 1. Iterate over all arcs
498 // 2. Sort all multi
499 var anchors = {};
500
501 // 1. Sort by length
502 // 2. Tag all spans with the number of overlaps before
503 // a) Iterate over all spans
504 // b) check the latest preceeding overlapping span (lpos)
505 // -> not found: tag with 0
506 // -> found: Add +1 to the level of the (lpos)
507 // c) If the new tag is smaller than the previous element,
508 // reorder
509
510 // Normalize start and end
511 var sortedArcs = this._arcs.map(function (v) {
512
513 // Check for long anchors
514 if (v.start instanceof Array) {
515
516 if (v.start[0] == v.start[1]) {
517 v.start = v.start[0];
518 }
519
520 else {
521
522 var middle = Math.ceil(Math.abs(v.start[1] - v.start[0]) / 2) + v.start[0];
523
524 // Calculate signature to avoid multiple anchors
525 var anchorSig = "#" + v.start[0] + "_" + v.start[1];
526 if (v.start[0] > v.start[1]) {
527 anchorSig = "#" + v.start[1] + "_" + v.start[0];
528 };
529
530 // Check if the anchor already exist
531 var anchor = anchors[anchorSig];
532 if (anchor === undefined) {
533 anchor = {
534 "first": v.start[0],
535 "last" : v.start[1],
536 "length" : v.start[1] - v.start[0]
537 };
538 anchors[anchorSig] = anchor;
539 // anchors.push(v.startAnchor);
540 };
541
542 v.startAnchor = anchor;
543
544 // Add to anchors list
545 v.start = middle;
546 };
547 };
548
549 if (v.end instanceof Array) {
550
551 if (v.end[0] == v.end[1]) {
552 v.end = v.end[0];
553 }
554
555 else {
556
557 var middle = Math.abs(v.end[0] - v.end[1]) + v.end[0];
558
559 // Calculate signature to avoid multiple anchors
560 var anchorSig = "#" + v.end[0] + "_" + v.end[1];
561 if (v.end[0] > v.end[1]) {
562 anchorSig = "#" + v.end[1] + "_" + v.end[0];
563 };
564
565 // Check if the anchor already exist
566 var anchor = anchors[anchorSig];
567 if (anchor === undefined) {
568 anchor = {
569 "first": v.end[0],
570 "last" : v.end[1],
571 "length" : v.end[1] - v.end[0]
572 };
573 anchors[anchorSig] = anchor;
574 // anchors.push(v.startAnchor);
575 };
576
577 v.endAnchor = anchor;
578
579 // Add to anchors list
580 // anchors.push(v.endAnchor);
581 v.end = middle;
582 };
583 };
584
585 v.first = v.start;
586 v.last = v.end;
587
588 // calculate the arch length
589 if (v.start < v.end) {
590 v.length = v.end - v.start;
591 }
592 else {
593 // v.first = v.end;
594 // v.last = v.start;
595 v.length = v.start - v.end;
596 };
597
598 return v;
599 });
600
601 // Sort based on length
602 sortedArcs.sort(function (a, b) {
603 if (a.length < b.length)
604 return -1;
605 else
606 return 1;
607 });
608
609 // Add sorted arcs and anchors
610 this._sortedArcs = lengthSort(sortedArcs, false);
611
612 // Translate map to array (there is probably a better JS method)
613 var sortedAnchors = [];
614 for (var i in anchors) {
615 sortedAnchors.push(anchors[i]);
616 };
617 this._sortedAnchors = lengthSort(sortedAnchors, true);
618 },
619
620 /**
621 * Center the viewport of the canvas
622 * TODO:
623 * This is identical to tree
624 */
625 center : function () {
626 if (this._element === undefined)
627 return;
628
629 var treeDiv = this._element.parentNode;
630
631 var cWidth = parseFloat(window.getComputedStyle(this._element).width);
632 var treeWidth = parseFloat(window.getComputedStyle(treeDiv).width);
633 // Reposition:
634 if (cWidth > treeWidth) {
635 var scrollValue = (cWidth - treeWidth) / 2;
636 treeDiv.scrollLeft = scrollValue;
637 };
638 },
639
640
641 // Show the element
642 show : function () {
643 var svg = this._element;
644 var height = this.maxArc;
645
646 // Delete old group
647 if (svg.getElementsByTagName("g")[0] !== undefined) {
648 var group = svg.getElementsByTagName("g")[0];
649 svg.removeChild(group);
650 this._tokenElements = [];
651 };
652
653 var g = svg.appendChild(this._c("g"));
654
655 // Draw token list
656 var text = g.appendChild(this._c("text"));
657 text.setAttribute('class', 'leaf');
658 text.setAttribute("text-anchor", "start");
659 text.setAttribute("y", height);
660
661 // Calculate the start position
662 this._y = height - (this.anchorStart);
663
664 // Introduce some prepending whitespace (yeah - I know ...)
665 var ws = text.appendChild(this._c("tspan"));
Akron0b489ad2018-02-02 16:49:32 +0100666 ws.appendChild(d.createTextNode('\u00A0'));
Akron3ebfd4e2017-11-13 17:56:49 +0100667 ws.style.textAnchor = "start";
668
669 var lastRight = 0;
670 for (var node_i in this._tokens) {
671 // Append svg
672 // var x = text.appendChild(this._c("text"));
673 var tspan = text.appendChild(this._c("tspan"));
Akron0b489ad2018-02-02 16:49:32 +0100674 tspan.appendChild(d.createTextNode(this._tokens[node_i]));
Akron3ebfd4e2017-11-13 17:56:49 +0100675 tspan.setAttribute("text-anchor", "middle");
676
677 this._tokenElements.push(tspan);
678
679 // Add whitespace!
680 tspan.setAttribute("dx", this.tokenSep);
681 };
682
683 // Get some global position data that may change on resize
684 var globalBoundingBox = this._rect(g);
685 this.offsetLeft = globalBoundingBox.left;
686
687 // The group of arcs
688 var arcs = g.appendChild(this._c("g"));
689 this._arcsElement = arcs;
690 arcs.classList.add("arcs");
691
692 var labels = g.appendChild(this._c("g"));
693 this._labelsElement = labels;
694 labels.classList.add("labels");
695
696 // Sort arcs if not sorted yet
697 if (this._sortedArcs === undefined)
698 this._sortArcs();
699
700 // 1. Draw all anchors
701 var i;
702 for (i in this._sortedAnchors) {
703 this._drawAnchor(this._sortedAnchors[i]);
704 };
705
706 // 2. Draw all arcs
707 for (i in this._sortedArcs) {
708 this._drawArc(this._sortedArcs[i]);
709 };
710
711 // Resize the svg with some reasonable margins
712 var width = this._rect(text).width;
713 svg.setAttribute("width", width + 20);
714 svg.setAttribute("height", height + 20);
715 svg.setAttribute("class", "relTree");
716 }
717 };
718
719 // Sort relations regarding their span
720 function lengthSort (list, inclusive) {
721
722 /*
723 * The "inclusive" flag allows to
724 * modify the behaviour for inclusivity check,
725 * e.g. if identical start or endpoints mean overlap or not.
726 */
727
728 var stack = [];
729
730 // Iterate over all definitions
731 for (var i = 0; i < list.length; i++) {
732 var current = list[i];
733
734 // Check the stack order
735 var overlaps = 0;
736 for (var j = (stack.length - 1); j >= 0; j--) {
737 var check = stack[j];
738
739 // (a..(b..b)..a)
740 if (current.first <= check.first && current.last >= check.last) {
741 overlaps = check.overlaps + 1;
742 break;
743 }
744
745 // (a..(b..a)..b)
746 else if (current.first <= check.first && current.last >= check.first) {
747
748 if (inclusive || (current.first != check.first && current.last != check.first)) {
749 overlaps = check.overlaps + (current.length == check.length ? 0 : 1);
750 };
751 }
752
753 // (b..(a..b)..a)
754 else if (current.first <= check.last && current.last >= check.last) {
755
756 if (inclusive || (current.first != check.last && current.last != check.last)) {
757 overlaps = check.overlaps + (current.length == check.length ? 0 : 1);
758 };
759 };
760 };
761
762 // Set overlaps
763 current.overlaps = overlaps;
764
765 stack.push(current);
766
767 // Although it is already sorted,
768 // the new item has to be put at the correct place
769 // TODO:
770 // Use something like splice() instead
771 stack.sort(function (a,b) {
772 b.overlaps - a.overlaps
773 });
774 };
775
776 return stack;
777 };
778});