blob: 0a7bf4992028e46b8360fb9b4f712345abcba972 [file] [log] [blame]
Nils Diewald0e6992a2015-04-14 20:13:52 +00001/**
Nils Diewald7148c6f2015-05-04 15:07:53 +00002 * Visualize span annotations as a tree
3 * using Dagre.
Akron7524be12016-06-01 17:31:33 +02004 *
5 * This should be lazy loaded!
Nils Diewald0e6992a2015-04-14 20:13:52 +00006 */
7define(['lib/dagre'], function (dagre) {
8 "use strict";
9
Akron0b489ad2018-02-02 16:49:32 +010010 const d = document;
11 const svgNS = "http://www.w3.org/2000/svg";
12 const _TermRE = new RegExp("^(?:([^\/]+?)\/)?([^:]+?):(.+?)$");
Nils Diewald0e6992a2015-04-14 20:13:52 +000013
Nils Diewald7148c6f2015-05-04 15:07:53 +000014 // Node size
Nils Diewald4347ee92015-05-04 20:32:48 +000015 var WIDTH = 55, HEIGHT = 20, LINEHEIGHT = 14;
Nils Diewald7148c6f2015-05-04 15:07:53 +000016
Nils Diewald0e6992a2015-04-14 20:13:52 +000017 // Create path for node connections
18 function _line (src, target) {
19 var x1 = src.x,
20 y1 = src.y,
21 x2 = target.x,
22 y2 = target.y - target.height / 2;
23
24 // c 0,0 -10,0
25 return 'M ' + x1 + ',' + y1 + ' ' +
26 'C ' + x1 + ',' + y1 + ' ' +
27 x2 + ',' + (y2 - (y2 - y1) / 2) + ' ' +
28 x2 + ',' + y2;
29 };
30
31 return {
Nils Diewald7148c6f2015-05-04 15:07:53 +000032
33 /**
34 * Create new tree visualization based
35 * on a match snippet.
36 */
Nils Diewald0e6992a2015-04-14 20:13:52 +000037 create : function (snippet) {
Nils Diewald7148c6f2015-05-04 15:07:53 +000038 return Object.create(this).
Akronc56cf2d2016-11-09 22:02:38 +010039 _init(snippet);
Nils Diewald0e6992a2015-04-14 20:13:52 +000040 },
41
Nils Diewald0e6992a2015-04-14 20:13:52 +000042
Nils Diewald7148c6f2015-05-04 15:07:53 +000043 // Initialize the tree based on a snippet.
Nils Diewald0e6992a2015-04-14 20:13:52 +000044 _init : function (snippet) {
45 this._next = new Number(0);
Akronc56cf2d2016-11-09 22:02:38 +010046
Nils Diewald0e6992a2015-04-14 20:13:52 +000047 // Create html for traversal
Akron0b489ad2018-02-02 16:49:32 +010048 var html = d.createElement("div");
Nils Diewald0e6992a2015-04-14 20:13:52 +000049 html.innerHTML = snippet;
50 var g = new dagre.graphlib.Graph({
Akronc56cf2d2016-11-09 22:02:38 +010051 "directed" : true
Nils Diewald0e6992a2015-04-14 20:13:52 +000052 });
53 g.setGraph({
Akronc56cf2d2016-11-09 22:02:38 +010054 "nodesep" : 35,
55 "ranksep" : 15,
56 "marginx" : 40,
57 "marginy" : 10
Nils Diewald0e6992a2015-04-14 20:13:52 +000058 });
59 g.setDefaultEdgeLabel({});
60
61 this._graph = g;
62
63 // This is a new root
64 this._addNode(
Akronc56cf2d2016-11-09 22:02:38 +010065 this._next++,
66 { "class" : "root" }
Nils Diewald0e6992a2015-04-14 20:13:52 +000067 );
68
69 // Parse nodes from root
Akron98a933f2016-08-11 00:19:17 +020070 this._parse(0, html.childNodes, undefined);
Nils Diewald0e6992a2015-04-14 20:13:52 +000071
72 // Root node has only one child - remove
73 if (g.outEdges(0).length === 1)
Akronc56cf2d2016-11-09 22:02:38 +010074 g.removeNode(0);
Nils Diewald0e6992a2015-04-14 20:13:52 +000075
76 html = undefined;
77 return this;
78 },
79
Akron0b489ad2018-02-02 16:49:32 +010080
81 _c : function (tag) {
82 return d.createElementNS(svgNS, tag);
83 },
84
Nils Diewald7148c6f2015-05-04 15:07:53 +000085 /**
86 * The number of nodes in the tree.
87 */
88 nodes : function () {
89 return this._next;
90 },
91
92 // Add new node to graph
93 _addNode : function (id, obj) {
94 obj["width"] = WIDTH;
95 obj["height"] = HEIGHT;
96 this._graph.setNode(id, obj)
Akron98a933f2016-08-11 00:19:17 +020097 return obj;
Nils Diewald7148c6f2015-05-04 15:07:53 +000098 },
99
100 // Add new edge to graph
101 _addEdge : function (src, target) {
102 this._graph.setEdge(src, target);
103 },
104
Nils Diewald0e6992a2015-04-14 20:13:52 +0000105 // Remove foundry and layer for labels
106 _clean : function (title) {
107 return title.replace(_TermRE, "$3");
108 },
109
110 // Parse the snippet
Akron98a933f2016-08-11 00:19:17 +0200111 _parse : function (parent, children, mark) {
Akron678c26f2020-10-09 08:52:50 +0200112 children.forEach(function(c) {
Nils Diewald0e6992a2015-04-14 20:13:52 +0000113
Akronc56cf2d2016-11-09 22:02:38 +0100114 // Element node
115 if (c.nodeType == 1) {
Nils Diewald0e6992a2015-04-14 20:13:52 +0000116
Akronc56cf2d2016-11-09 22:02:38 +0100117 // Get title from html
118 if (c.getAttribute("title")) {
119 var title = this._clean(c.getAttribute("title"));
Nils Diewald0e6992a2015-04-14 20:13:52 +0000120
Akronc56cf2d2016-11-09 22:02:38 +0100121 // Add child node
122 var id = this._next++;
Nils Diewald0e6992a2015-04-14 20:13:52 +0000123
Akronc56cf2d2016-11-09 22:02:38 +0100124 var obj = this._addNode(id, {
125 "class" : "middle",
126 "label" : title
127 });
Akron98a933f2016-08-11 00:19:17 +0200128
129 if (mark !== undefined) {
130 obj.class += ' mark';
131 };
132
Akronc56cf2d2016-11-09 22:02:38 +0100133 this._addEdge(parent, id);
Nils Diewald0e6992a2015-04-14 20:13:52 +0000134
Akronc56cf2d2016-11-09 22:02:38 +0100135 // Check for next level
136 if (c.hasChildNodes())
137 this._parse(id, c.childNodes, mark);
138 }
Nils Diewald0e6992a2015-04-14 20:13:52 +0000139
Akronc56cf2d2016-11-09 22:02:38 +0100140 // Step further
141 else if (c.hasChildNodes()) {
Nils Diewald0e6992a2015-04-14 20:13:52 +0000142
Akron98a933f2016-08-11 00:19:17 +0200143 if (c.tagName === 'MARK') {
Akronc56cf2d2016-11-09 22:02:38 +0100144 this._parse(parent, c.childNodes, true);
Akron98a933f2016-08-11 00:19:17 +0200145 }
146 else {
Akronc56cf2d2016-11-09 22:02:38 +0100147 this._parse(parent, c.childNodes, mark);
Akron98a933f2016-08-11 00:19:17 +0200148 };
149 };
Akronc56cf2d2016-11-09 22:02:38 +0100150 }
Nils Diewald0e6992a2015-04-14 20:13:52 +0000151
Akronc56cf2d2016-11-09 22:02:38 +0100152 // Text node
153 else if (c.nodeType == 3)
Nils Diewald0e6992a2015-04-14 20:13:52 +0000154
Akronc56cf2d2016-11-09 22:02:38 +0100155 if (c.nodeValue.match(/[-a-z0-9]/i)) {
Nils Diewald0e6992a2015-04-14 20:13:52 +0000156
Akronc56cf2d2016-11-09 22:02:38 +0100157 // Add child node
158 var id = this._next++;
159 this._addNode(id, {
160 "class" : "leaf",
161 "label" : c.nodeValue
162 });
Akron98a933f2016-08-11 00:19:17 +0200163
Akronc56cf2d2016-11-09 22:02:38 +0100164 this._addEdge(parent, id);
165 };
Akron678c26f2020-10-09 08:52:50 +0200166 }, this);
Nils Diewald0e6992a2015-04-14 20:13:52 +0000167 return this;
168 },
169
Akron0988d882017-11-10 16:13:12 +0100170 // Dummy method to be compatible with relTree
171 show : function () {
172 return;
173 },
174
Nils Diewald0e6992a2015-04-14 20:13:52 +0000175 /**
176 * Center the viewport of the canvas
Akron0988d882017-11-10 16:13:12 +0100177 * TODO:
178 * This is identical to relations
Nils Diewald0e6992a2015-04-14 20:13:52 +0000179 */
180 center : function () {
181 if (this._element === undefined)
Akronc56cf2d2016-11-09 22:02:38 +0100182 return;
Nils Diewald0e6992a2015-04-14 20:13:52 +0000183
184 var treeDiv = this._element.parentNode;
185
186 var cWidth = parseFloat(window.getComputedStyle(this._element).width);
187 var treeWidth = parseFloat(window.getComputedStyle(treeDiv).width);
188 // Reposition:
189 if (cWidth > treeWidth) {
Akronc56cf2d2016-11-09 22:02:38 +0100190 var scrollValue = (cWidth - treeWidth) / 2;
191 treeDiv.scrollLeft = scrollValue;
Nils Diewald0e6992a2015-04-14 20:13:52 +0000192 };
193 },
194
Akronc56cf2d2016-11-09 22:02:38 +0100195
Akron151bc872018-02-02 14:04:15 +0100196 /**
197 * Create svg and serialize as base64
198 */
Akronc56cf2d2016-11-09 22:02:38 +0100199 toBase64 : function () {
Akron6f32f822016-11-10 00:23:40 +0100200
201 // First clone element
Akron0b489ad2018-02-02 16:49:32 +0100202 var svgWrapper = d.createElement('div')
Akron6f32f822016-11-10 00:23:40 +0100203 svgWrapper.innerHTML = this.element().outerHTML;
204 var svg = svgWrapper.firstChild;
205
Akron0b489ad2018-02-02 16:49:32 +0100206 var style = this._c('style');
Akron6f32f822016-11-10 00:23:40 +0100207 svg.getElementsByTagName('defs')[0].appendChild(style);
208
209 style.innerHTML =
210 'path.edge ' + '{ stroke: black; stroke-width: 2pt; fill: none; }' +
211 'g.root rect.empty,' +
212 'g.middle rect' + '{ stroke: black; stroke-width: 2pt; fill: #bbb; }' +
213 'g.leaf > rect ' + '{ display: none }' +
214 'g > text > tspan ' + '{ text-anchor: middle; font-size: 9pt }' +
215 'g.leaf > text > tspan ' + '{ font-size: 10pt; overflow: visible; }';
Akron3bdac532019-03-04 13:24:43 +0100216
217 return btoa(unescape(encodeURIComponent(svg.outerHTML)).replace(/ /g, ' '));
Akronc56cf2d2016-11-09 22:02:38 +0100218 },
219
Nils Diewald7148c6f2015-05-04 15:07:53 +0000220 /**
221 * Get the dom element of the tree view.
222 */
Nils Diewald0e6992a2015-04-14 20:13:52 +0000223 element : function () {
224 if (this._element !== undefined)
Akronc56cf2d2016-11-09 22:02:38 +0100225 return this._element;
Nils Diewald0e6992a2015-04-14 20:13:52 +0000226
227 var g = this._graph;
228
229 dagre.layout(g);
Akronc56cf2d2016-11-09 22:02:38 +0100230
Akron0b489ad2018-02-02 16:49:32 +0100231 var canvas = this._c('svg');
Nils Diewald0e6992a2015-04-14 20:13:52 +0000232 this._element = canvas;
Akron0b489ad2018-02-02 16:49:32 +0100233 var that = this;
Nils Diewald0e6992a2015-04-14 20:13:52 +0000234
Akron0b489ad2018-02-02 16:49:32 +0100235 canvas.appendChild(this._c('defs'));
Akron6f32f822016-11-10 00:23:40 +0100236
Nils Diewald4347ee92015-05-04 20:32:48 +0000237 var height = g.graph().height;
Nils Diewald0e6992a2015-04-14 20:13:52 +0000238
239 // Create edges
240 g.edges().forEach(
Akronc56cf2d2016-11-09 22:02:38 +0100241 function (e) {
242 var src = g.node(e.v);
243 var target = g.node(e.w);
Akron0b489ad2018-02-02 16:49:32 +0100244 var p = that._c('path');
Akronc56cf2d2016-11-09 22:02:38 +0100245 p.setAttributeNS(null, "d", _line(src, target));
246 p.classList.add('edge');
247 canvas.appendChild(p);
248 });
Nils Diewald0e6992a2015-04-14 20:13:52 +0000249
250 // Create nodes
251 g.nodes().forEach(
Akronc56cf2d2016-11-09 22:02:38 +0100252 function (v) {
253 v = g.node(v);
Akron0b489ad2018-02-02 16:49:32 +0100254 var group = that._c('g');
Akronc56cf2d2016-11-09 22:02:38 +0100255 group.setAttribute('class', v.class);
256
257 // Add node box
Akron0b489ad2018-02-02 16:49:32 +0100258 var rect = group.appendChild(that._c('rect'));
Akronc56cf2d2016-11-09 22:02:38 +0100259 rect.setAttribute('x', v.x - v.width / 2);
260 rect.setAttribute('y', v.y - v.height / 2);
261 rect.setAttribute('rx', 5);
262 rect.setAttribute('ry', 5);
263 rect.setAttribute('width', v.width);
264 rect.setAttribute('height', v.height);
Nils Diewald0e6992a2015-04-14 20:13:52 +0000265
Akronc56cf2d2016-11-09 22:02:38 +0100266 if (v.class === 'root' && v.label === undefined) {
267 rect.setAttribute('width', v.height);
268 rect.setAttribute('x', v.x - v.height / 2);
269 rect.setAttribute('class', 'empty');
270 };
Nils Diewald0e6992a2015-04-14 20:13:52 +0000271
Akronc56cf2d2016-11-09 22:02:38 +0100272 // Add label
273 if (v.label !== undefined) {
Akron0b489ad2018-02-02 16:49:32 +0100274 var text = group.appendChild(that._c('text'));
Akronc56cf2d2016-11-09 22:02:38 +0100275 var y = v.y - v.height / 2;
276 text.setAttribute('y', y);
277 text.setAttribute(
278 'transform',
279 'translate(' + v.width/2 + ',' + ((v.height / 2) + 5) + ')'
280 );
Akron3bdac532019-03-04 13:24:43 +0100281
282 var vLabel = v.label.replace(/ /g, " ")
283 .replace(/&/g, '&')
284 .replace(/&lt;/g, '<')
285 .replace(/&gt;/g, '>');
Akronc56cf2d2016-11-09 22:02:38 +0100286
287 if (v.class === "leaf") {
Akron3bdac532019-03-04 13:24:43 +0100288 text.setAttribute('title', vLabel);
Nils Diewald0e6992a2015-04-14 20:13:52 +0000289
Akron3bdac532019-03-04 13:24:43 +0100290 var labelPart = vLabel.split(" ");
Akronc56cf2d2016-11-09 22:02:38 +0100291 var n = 0;
292 for (var i = 0; i < labelPart.length; i++) {
293 if (labelPart[i].length === 0)
294 continue;
Nils Diewald0e6992a2015-04-14 20:13:52 +0000295
Akron0b489ad2018-02-02 16:49:32 +0100296 var tspan = that._c('tspan');
297 tspan.appendChild(d.createTextNode(labelPart[i]));
Akronc56cf2d2016-11-09 22:02:38 +0100298 if (n !== 0)
299 tspan.setAttribute('dy', LINEHEIGHT + 'pt');
300 else
301 n = 1;
302 tspan.setAttribute('x', v.x - v.width / 2);
303 y += LINEHEIGHT;
304 text.appendChild(tspan);
305 };
Nils Diewald0e6992a2015-04-14 20:13:52 +0000306
Akronc56cf2d2016-11-09 22:02:38 +0100307 y += LINEHEIGHT;
Nils Diewald4347ee92015-05-04 20:32:48 +0000308
Akronc56cf2d2016-11-09 22:02:38 +0100309 // The text is below the canvas - readjust the height!
310 if (y > height)
311 height = y;
312 }
313 else {
Akron0b489ad2018-02-02 16:49:32 +0100314 var tspan = that._c('tspan');
Akron3bdac532019-03-04 13:24:43 +0100315 tspan.appendChild(d.createTextNode(vLabel));
Akronc56cf2d2016-11-09 22:02:38 +0100316 tspan.setAttribute('x', v.x - v.width / 2);
317 text.appendChild(tspan);
318 };
319 };
320 canvas.appendChild(group);
321 }
Nils Diewald0e6992a2015-04-14 20:13:52 +0000322 );
323
Nils Diewald4347ee92015-05-04 20:32:48 +0000324 canvas.setAttribute('width', g.graph().width);
325 canvas.setAttribute('height', height);
Nils Diewald0e6992a2015-04-14 20:13:52 +0000326 return this._element;
Akron151bc872018-02-02 14:04:15 +0100327 },
328
329 downloadLink : function () {
Akron0b489ad2018-02-02 16:49:32 +0100330 var a = d.createElement('a');
Akron151bc872018-02-02 14:04:15 +0100331 a.setAttribute('href-lang', 'image/svg+xml');
332 a.setAttribute('href', 'data:image/svg+xml;base64,' + this.toBase64());
333 a.setAttribute('download', 'tree.svg');
334 a.target = '_blank';
335 a.setAttribute('rel', 'noopener noreferrer');
336 return a;
Nils Diewald0e6992a2015-04-14 20:13:52 +0000337 }
338 };
339});