blob: e9c61223326cc4f0439b545a0ac43475d14793dd [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) {
Nils Diewald0e6992a2015-04-14 20:13:52 +0000112 for (var i in children) {
Akronc56cf2d2016-11-09 22:02:38 +0100113 var c = children[i];
Nils Diewald0e6992a2015-04-14 20:13:52 +0000114
Akronc56cf2d2016-11-09 22:02:38 +0100115 // Element node
116 if (c.nodeType == 1) {
Nils Diewald0e6992a2015-04-14 20:13:52 +0000117
Akronc56cf2d2016-11-09 22:02:38 +0100118 // Get title from html
119 if (c.getAttribute("title")) {
120 var title = this._clean(c.getAttribute("title"));
Nils Diewald0e6992a2015-04-14 20:13:52 +0000121
Akronc56cf2d2016-11-09 22:02:38 +0100122 // Add child node
123 var id = this._next++;
Nils Diewald0e6992a2015-04-14 20:13:52 +0000124
Akronc56cf2d2016-11-09 22:02:38 +0100125 var obj = this._addNode(id, {
126 "class" : "middle",
127 "label" : title
128 });
Akron98a933f2016-08-11 00:19:17 +0200129
130 if (mark !== undefined) {
131 obj.class += ' mark';
132 };
133
Akronc56cf2d2016-11-09 22:02:38 +0100134 this._addEdge(parent, id);
Nils Diewald0e6992a2015-04-14 20:13:52 +0000135
Akronc56cf2d2016-11-09 22:02:38 +0100136 // Check for next level
137 if (c.hasChildNodes())
138 this._parse(id, c.childNodes, mark);
139 }
Nils Diewald0e6992a2015-04-14 20:13:52 +0000140
Akronc56cf2d2016-11-09 22:02:38 +0100141 // Step further
142 else if (c.hasChildNodes()) {
Nils Diewald0e6992a2015-04-14 20:13:52 +0000143
Akron98a933f2016-08-11 00:19:17 +0200144 if (c.tagName === 'MARK') {
Akronc56cf2d2016-11-09 22:02:38 +0100145 this._parse(parent, c.childNodes, true);
Akron98a933f2016-08-11 00:19:17 +0200146 }
147 else {
Akronc56cf2d2016-11-09 22:02:38 +0100148 this._parse(parent, c.childNodes, mark);
Akron98a933f2016-08-11 00:19:17 +0200149 };
150 };
Akronc56cf2d2016-11-09 22:02:38 +0100151 }
Nils Diewald0e6992a2015-04-14 20:13:52 +0000152
Akronc56cf2d2016-11-09 22:02:38 +0100153 // Text node
154 else if (c.nodeType == 3)
Nils Diewald0e6992a2015-04-14 20:13:52 +0000155
Akronc56cf2d2016-11-09 22:02:38 +0100156 if (c.nodeValue.match(/[-a-z0-9]/i)) {
Nils Diewald0e6992a2015-04-14 20:13:52 +0000157
Akronc56cf2d2016-11-09 22:02:38 +0100158 // Add child node
159 var id = this._next++;
160 this._addNode(id, {
161 "class" : "leaf",
162 "label" : c.nodeValue
163 });
Akron98a933f2016-08-11 00:19:17 +0200164
Akronc56cf2d2016-11-09 22:02:38 +0100165 this._addEdge(parent, id);
166 };
Nils Diewald0e6992a2015-04-14 20:13:52 +0000167 };
168 return this;
169 },
170
Akron0988d882017-11-10 16:13:12 +0100171 // Dummy method to be compatible with relTree
172 show : function () {
173 return;
174 },
175
Nils Diewald0e6992a2015-04-14 20:13:52 +0000176 /**
177 * Center the viewport of the canvas
Akron0988d882017-11-10 16:13:12 +0100178 * TODO:
179 * This is identical to relations
Nils Diewald0e6992a2015-04-14 20:13:52 +0000180 */
181 center : function () {
182 if (this._element === undefined)
Akronc56cf2d2016-11-09 22:02:38 +0100183 return;
Nils Diewald0e6992a2015-04-14 20:13:52 +0000184
185 var treeDiv = this._element.parentNode;
186
187 var cWidth = parseFloat(window.getComputedStyle(this._element).width);
188 var treeWidth = parseFloat(window.getComputedStyle(treeDiv).width);
189 // Reposition:
190 if (cWidth > treeWidth) {
Akronc56cf2d2016-11-09 22:02:38 +0100191 var scrollValue = (cWidth - treeWidth) / 2;
192 treeDiv.scrollLeft = scrollValue;
Nils Diewald0e6992a2015-04-14 20:13:52 +0000193 };
194 },
195
Akronc56cf2d2016-11-09 22:02:38 +0100196
Akron151bc872018-02-02 14:04:15 +0100197 /**
198 * Create svg and serialize as base64
199 */
Akronc56cf2d2016-11-09 22:02:38 +0100200 toBase64 : function () {
Akron6f32f822016-11-10 00:23:40 +0100201
202 // First clone element
Akron0b489ad2018-02-02 16:49:32 +0100203 var svgWrapper = d.createElement('div')
Akron6f32f822016-11-10 00:23:40 +0100204 svgWrapper.innerHTML = this.element().outerHTML;
205 var svg = svgWrapper.firstChild;
206
Akron0b489ad2018-02-02 16:49:32 +0100207 var style = this._c('style');
Akron6f32f822016-11-10 00:23:40 +0100208 svg.getElementsByTagName('defs')[0].appendChild(style);
209
210 style.innerHTML =
211 'path.edge ' + '{ stroke: black; stroke-width: 2pt; fill: none; }' +
212 'g.root rect.empty,' +
213 'g.middle rect' + '{ stroke: black; stroke-width: 2pt; fill: #bbb; }' +
214 'g.leaf > rect ' + '{ display: none }' +
215 'g > text > tspan ' + '{ text-anchor: middle; font-size: 9pt }' +
216 'g.leaf > text > tspan ' + '{ font-size: 10pt; overflow: visible; }';
Akron3bdac532019-03-04 13:24:43 +0100217
218 return btoa(unescape(encodeURIComponent(svg.outerHTML)).replace(/ /g, ' '));
Akronc56cf2d2016-11-09 22:02:38 +0100219 },
220
Nils Diewald7148c6f2015-05-04 15:07:53 +0000221 /**
222 * Get the dom element of the tree view.
223 */
Nils Diewald0e6992a2015-04-14 20:13:52 +0000224 element : function () {
225 if (this._element !== undefined)
Akronc56cf2d2016-11-09 22:02:38 +0100226 return this._element;
Nils Diewald0e6992a2015-04-14 20:13:52 +0000227
228 var g = this._graph;
229
230 dagre.layout(g);
Akronc56cf2d2016-11-09 22:02:38 +0100231
Akron0b489ad2018-02-02 16:49:32 +0100232 var canvas = this._c('svg');
Nils Diewald0e6992a2015-04-14 20:13:52 +0000233 this._element = canvas;
Akron0b489ad2018-02-02 16:49:32 +0100234 var that = this;
Nils Diewald0e6992a2015-04-14 20:13:52 +0000235
Akron0b489ad2018-02-02 16:49:32 +0100236 canvas.appendChild(this._c('defs'));
Akron6f32f822016-11-10 00:23:40 +0100237
Nils Diewald4347ee92015-05-04 20:32:48 +0000238 var height = g.graph().height;
Nils Diewald0e6992a2015-04-14 20:13:52 +0000239
240 // Create edges
241 g.edges().forEach(
Akronc56cf2d2016-11-09 22:02:38 +0100242 function (e) {
243 var src = g.node(e.v);
244 var target = g.node(e.w);
Akron0b489ad2018-02-02 16:49:32 +0100245 var p = that._c('path');
Akronc56cf2d2016-11-09 22:02:38 +0100246 p.setAttributeNS(null, "d", _line(src, target));
247 p.classList.add('edge');
248 canvas.appendChild(p);
249 });
Nils Diewald0e6992a2015-04-14 20:13:52 +0000250
251 // Create nodes
252 g.nodes().forEach(
Akronc56cf2d2016-11-09 22:02:38 +0100253 function (v) {
254 v = g.node(v);
Akron0b489ad2018-02-02 16:49:32 +0100255 var group = that._c('g');
Akronc56cf2d2016-11-09 22:02:38 +0100256 group.setAttribute('class', v.class);
257
258 // Add node box
Akron0b489ad2018-02-02 16:49:32 +0100259 var rect = group.appendChild(that._c('rect'));
Akronc56cf2d2016-11-09 22:02:38 +0100260 rect.setAttribute('x', v.x - v.width / 2);
261 rect.setAttribute('y', v.y - v.height / 2);
262 rect.setAttribute('rx', 5);
263 rect.setAttribute('ry', 5);
264 rect.setAttribute('width', v.width);
265 rect.setAttribute('height', v.height);
Nils Diewald0e6992a2015-04-14 20:13:52 +0000266
Akronc56cf2d2016-11-09 22:02:38 +0100267 if (v.class === 'root' && v.label === undefined) {
268 rect.setAttribute('width', v.height);
269 rect.setAttribute('x', v.x - v.height / 2);
270 rect.setAttribute('class', 'empty');
271 };
Nils Diewald0e6992a2015-04-14 20:13:52 +0000272
Akronc56cf2d2016-11-09 22:02:38 +0100273 // Add label
274 if (v.label !== undefined) {
Akron0b489ad2018-02-02 16:49:32 +0100275 var text = group.appendChild(that._c('text'));
Akronc56cf2d2016-11-09 22:02:38 +0100276 var y = v.y - v.height / 2;
277 text.setAttribute('y', y);
278 text.setAttribute(
279 'transform',
280 'translate(' + v.width/2 + ',' + ((v.height / 2) + 5) + ')'
281 );
Akron3bdac532019-03-04 13:24:43 +0100282
283 var vLabel = v.label.replace(/ /g, " ")
284 .replace(/&/g, '&')
285 .replace(/&lt;/g, '<')
286 .replace(/&gt;/g, '>');
Akronc56cf2d2016-11-09 22:02:38 +0100287
288 if (v.class === "leaf") {
Akron3bdac532019-03-04 13:24:43 +0100289 text.setAttribute('title', vLabel);
Nils Diewald0e6992a2015-04-14 20:13:52 +0000290
Akron3bdac532019-03-04 13:24:43 +0100291 var labelPart = vLabel.split(" ");
Akronc56cf2d2016-11-09 22:02:38 +0100292 var n = 0;
293 for (var i = 0; i < labelPart.length; i++) {
294 if (labelPart[i].length === 0)
295 continue;
Nils Diewald0e6992a2015-04-14 20:13:52 +0000296
Akron0b489ad2018-02-02 16:49:32 +0100297 var tspan = that._c('tspan');
298 tspan.appendChild(d.createTextNode(labelPart[i]));
Akronc56cf2d2016-11-09 22:02:38 +0100299 if (n !== 0)
300 tspan.setAttribute('dy', LINEHEIGHT + 'pt');
301 else
302 n = 1;
303 tspan.setAttribute('x', v.x - v.width / 2);
304 y += LINEHEIGHT;
305 text.appendChild(tspan);
306 };
Nils Diewald0e6992a2015-04-14 20:13:52 +0000307
Akronc56cf2d2016-11-09 22:02:38 +0100308 y += LINEHEIGHT;
Nils Diewald4347ee92015-05-04 20:32:48 +0000309
Akronc56cf2d2016-11-09 22:02:38 +0100310 // The text is below the canvas - readjust the height!
311 if (y > height)
312 height = y;
313 }
314 else {
Akron0b489ad2018-02-02 16:49:32 +0100315 var tspan = that._c('tspan');
Akron3bdac532019-03-04 13:24:43 +0100316 tspan.appendChild(d.createTextNode(vLabel));
Akronc56cf2d2016-11-09 22:02:38 +0100317 tspan.setAttribute('x', v.x - v.width / 2);
318 text.appendChild(tspan);
319 };
320 };
321 canvas.appendChild(group);
322 }
Nils Diewald0e6992a2015-04-14 20:13:52 +0000323 );
324
Nils Diewald4347ee92015-05-04 20:32:48 +0000325 canvas.setAttribute('width', g.graph().width);
326 canvas.setAttribute('height', height);
Nils Diewald0e6992a2015-04-14 20:13:52 +0000327 return this._element;
Akron151bc872018-02-02 14:04:15 +0100328 },
329
330 downloadLink : function () {
Akron0b489ad2018-02-02 16:49:32 +0100331 var a = d.createElement('a');
Akron151bc872018-02-02 14:04:15 +0100332 a.setAttribute('href-lang', 'image/svg+xml');
333 a.setAttribute('href', 'data:image/svg+xml;base64,' + this.toBase64());
334 a.setAttribute('download', 'tree.svg');
335 a.target = '_blank';
336 a.setAttribute('rel', 'noopener noreferrer');
337 return a;
Nils Diewald0e6992a2015-04-14 20:13:52 +0000338 }
339 };
340});