blob: e54ef29a85c6593d3efa026b471178c8d3326982 [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) {
Akronff1b1f32020-10-18 11:41:29 +020019 const x1 = src.x,
20 y1 = src.y,
21 x2 = target.x,
22 y2 = target.y - target.height / 2;
Nils Diewald0e6992a2015-04-14 20:13:52 +000023
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).
Akronff1b1f32020-10-18 11:41:29 +020039 _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
Akronff1b1f32020-10-18 11:41:29 +020048 let html = d.createElement("div");
Nils Diewald0e6992a2015-04-14 20:13:52 +000049 html.innerHTML = snippet;
Akronff1b1f32020-10-18 11:41:29 +020050
51 const g = new dagre.graphlib.Graph({
52 "directed" : true
Nils Diewald0e6992a2015-04-14 20:13:52 +000053 });
Akronff1b1f32020-10-18 11:41:29 +020054
Nils Diewald0e6992a2015-04-14 20:13:52 +000055 g.setGraph({
Akronff1b1f32020-10-18 11:41:29 +020056 "nodesep" : 35,
57 "ranksep" : 15,
58 "marginx" : 40,
59 "marginy" : 10
Nils Diewald0e6992a2015-04-14 20:13:52 +000060 });
61 g.setDefaultEdgeLabel({});
62
63 this._graph = g;
64
65 // This is a new root
66 this._addNode(
Akronff1b1f32020-10-18 11:41:29 +020067 this._next++,
68 { "class" : "root" }
Nils Diewald0e6992a2015-04-14 20:13:52 +000069 );
70
71 // Parse nodes from root
Akron98a933f2016-08-11 00:19:17 +020072 this._parse(0, html.childNodes, undefined);
Nils Diewald0e6992a2015-04-14 20:13:52 +000073
74 // Root node has only one child - remove
75 if (g.outEdges(0).length === 1)
Akronff1b1f32020-10-18 11:41:29 +020076 g.removeNode(0);
Nils Diewald0e6992a2015-04-14 20:13:52 +000077
78 html = undefined;
79 return this;
80 },
81
Akron0b489ad2018-02-02 16:49:32 +010082
83 _c : function (tag) {
84 return d.createElementNS(svgNS, tag);
85 },
86
Akronff1b1f32020-10-18 11:41:29 +020087
Nils Diewald7148c6f2015-05-04 15:07:53 +000088 /**
89 * The number of nodes in the tree.
90 */
91 nodes : function () {
92 return this._next;
93 },
94
Akronff1b1f32020-10-18 11:41:29 +020095
Nils Diewald7148c6f2015-05-04 15:07:53 +000096 // Add new node to graph
97 _addNode : function (id, obj) {
98 obj["width"] = WIDTH;
99 obj["height"] = HEIGHT;
100 this._graph.setNode(id, obj)
Akron98a933f2016-08-11 00:19:17 +0200101 return obj;
Nils Diewald7148c6f2015-05-04 15:07:53 +0000102 },
103
Akronff1b1f32020-10-18 11:41:29 +0200104
Nils Diewald7148c6f2015-05-04 15:07:53 +0000105 // Add new edge to graph
106 _addEdge : function (src, target) {
107 this._graph.setEdge(src, target);
108 },
109
Akronff1b1f32020-10-18 11:41:29 +0200110
Nils Diewald0e6992a2015-04-14 20:13:52 +0000111 // Remove foundry and layer for labels
112 _clean : function (title) {
113 return title.replace(_TermRE, "$3");
114 },
115
Akronff1b1f32020-10-18 11:41:29 +0200116
Nils Diewald0e6992a2015-04-14 20:13:52 +0000117 // Parse the snippet
Akron98a933f2016-08-11 00:19:17 +0200118 _parse : function (parent, children, mark) {
Akron678c26f2020-10-09 08:52:50 +0200119 children.forEach(function(c) {
Nils Diewald0e6992a2015-04-14 20:13:52 +0000120
Akronff1b1f32020-10-18 11:41:29 +0200121 // Element node
122 if (c.nodeType == 1) {
Nils Diewald0e6992a2015-04-14 20:13:52 +0000123
Akronff1b1f32020-10-18 11:41:29 +0200124 // Get title from html
125 if (c.getAttribute("title")) {
Nils Diewald0e6992a2015-04-14 20:13:52 +0000126
Akronff1b1f32020-10-18 11:41:29 +0200127 // Add child node
128 const id = this._next++;
Nils Diewald0e6992a2015-04-14 20:13:52 +0000129
Akronff1b1f32020-10-18 11:41:29 +0200130 const obj = this._addNode(id, {
131 "class" : "middle",
132 "label" : this._clean(c.getAttribute("title"))
133 });
Akron98a933f2016-08-11 00:19:17 +0200134
135 if (mark !== undefined) {
136 obj.class += ' mark';
137 };
138
Akronff1b1f32020-10-18 11:41:29 +0200139 this._addEdge(parent, id);
Nils Diewald0e6992a2015-04-14 20:13:52 +0000140
Akronff1b1f32020-10-18 11:41:29 +0200141 // Check for next level
142 if (c.hasChildNodes())
143 this._parse(id, c.childNodes, mark);
144 }
Nils Diewald0e6992a2015-04-14 20:13:52 +0000145
Akronff1b1f32020-10-18 11:41:29 +0200146 // Step further
147 else if (c.hasChildNodes()) {
Nils Diewald0e6992a2015-04-14 20:13:52 +0000148
Akronff1b1f32020-10-18 11:41:29 +0200149 this._parse(
150 parent,
151 c.childNodes,
152 c.tagName === 'MARK' ? true : mark
153 );
Akron98a933f2016-08-11 00:19:17 +0200154 };
Akronff1b1f32020-10-18 11:41:29 +0200155 }
Nils Diewald0e6992a2015-04-14 20:13:52 +0000156
Akronff1b1f32020-10-18 11:41:29 +0200157 // Text node
158 else if (c.nodeType == 3)
Nils Diewald0e6992a2015-04-14 20:13:52 +0000159
Akronff1b1f32020-10-18 11:41:29 +0200160 if (c.nodeValue.match(/[-a-z0-9]/i)) {
161
162 // Add child node
163 const id = this._next++;
164 this._addNode(id, {
165 "class" : "leaf",
166 "label" : c.nodeValue
167 });
Nils Diewald0e6992a2015-04-14 20:13:52 +0000168
Akronff1b1f32020-10-18 11:41:29 +0200169 this._addEdge(parent, id);
170 };
Akron678c26f2020-10-09 08:52:50 +0200171 }, this);
Nils Diewald0e6992a2015-04-14 20:13:52 +0000172 return this;
173 },
174
Akronff1b1f32020-10-18 11:41:29 +0200175
Akron0988d882017-11-10 16:13:12 +0100176 // Dummy method to be compatible with relTree
177 show : function () {
178 return;
179 },
180
Akronff1b1f32020-10-18 11:41:29 +0200181
Nils Diewald0e6992a2015-04-14 20:13:52 +0000182 /**
183 * Center the viewport of the canvas
Akron0988d882017-11-10 16:13:12 +0100184 * TODO:
185 * This is identical to relations
Nils Diewald0e6992a2015-04-14 20:13:52 +0000186 */
187 center : function () {
188 if (this._element === undefined)
Akronff1b1f32020-10-18 11:41:29 +0200189 return;
Nils Diewald0e6992a2015-04-14 20:13:52 +0000190
Akronff1b1f32020-10-18 11:41:29 +0200191 const treeDiv = this._element.parentNode;
192 const cWidth = parseFloat(window.getComputedStyle(this._element).width);
193 const treeWidth = parseFloat(window.getComputedStyle(treeDiv).width);
Nils Diewald0e6992a2015-04-14 20:13:52 +0000194
Nils Diewald0e6992a2015-04-14 20:13:52 +0000195 // Reposition:
196 if (cWidth > treeWidth) {
Akronff1b1f32020-10-18 11:41:29 +0200197 treeDiv.scrollLeft = (cWidth - treeWidth) / 2;
Nils Diewald0e6992a2015-04-14 20:13:52 +0000198 };
199 },
200
Akronc56cf2d2016-11-09 22:02:38 +0100201
Akron151bc872018-02-02 14:04:15 +0100202 /**
203 * Create svg and serialize as base64
204 */
Akronc56cf2d2016-11-09 22:02:38 +0100205 toBase64 : function () {
Akron6f32f822016-11-10 00:23:40 +0100206
207 // First clone element
Akronff1b1f32020-10-18 11:41:29 +0200208 const svgWrapper = d.createElement('div')
Akron6f32f822016-11-10 00:23:40 +0100209 svgWrapper.innerHTML = this.element().outerHTML;
Akron6f32f822016-11-10 00:23:40 +0100210
Akronff1b1f32020-10-18 11:41:29 +0200211 const svg = svgWrapper.firstChild;
212 const style = this._c('style');
213
Akron6f32f822016-11-10 00:23:40 +0100214 svg.getElementsByTagName('defs')[0].appendChild(style);
215
216 style.innerHTML =
217 'path.edge ' + '{ stroke: black; stroke-width: 2pt; fill: none; }' +
218 'g.root rect.empty,' +
219 'g.middle rect' + '{ stroke: black; stroke-width: 2pt; fill: #bbb; }' +
220 'g.leaf > rect ' + '{ display: none }' +
221 'g > text > tspan ' + '{ text-anchor: middle; font-size: 9pt }' +
222 'g.leaf > text > tspan ' + '{ font-size: 10pt; overflow: visible; }';
Akron3bdac532019-03-04 13:24:43 +0100223
224 return btoa(unescape(encodeURIComponent(svg.outerHTML)).replace(/ /g, ' '));
Akronc56cf2d2016-11-09 22:02:38 +0100225 },
226
Akronff1b1f32020-10-18 11:41:29 +0200227
Nils Diewald7148c6f2015-05-04 15:07:53 +0000228 /**
229 * Get the dom element of the tree view.
230 */
Nils Diewald0e6992a2015-04-14 20:13:52 +0000231 element : function () {
Akronff1b1f32020-10-18 11:41:29 +0200232
Nils Diewald0e6992a2015-04-14 20:13:52 +0000233 if (this._element !== undefined)
Akronff1b1f32020-10-18 11:41:29 +0200234 return this._element;
Nils Diewald0e6992a2015-04-14 20:13:52 +0000235
Akronff1b1f32020-10-18 11:41:29 +0200236 const g = this._graph;
Nils Diewald0e6992a2015-04-14 20:13:52 +0000237 dagre.layout(g);
Akronc56cf2d2016-11-09 22:02:38 +0100238
Akronff1b1f32020-10-18 11:41:29 +0200239 const canvas = this._c('svg');
Nils Diewald0e6992a2015-04-14 20:13:52 +0000240 this._element = canvas;
241
Akron0b489ad2018-02-02 16:49:32 +0100242 canvas.appendChild(this._c('defs'));
Nils Diewald0e6992a2015-04-14 20:13:52 +0000243
244 // Create edges
Akronff1b1f32020-10-18 11:41:29 +0200245 const that = this;
246
247 let src, target, p;
248
Nils Diewald0e6992a2015-04-14 20:13:52 +0000249 g.edges().forEach(
Akronc56cf2d2016-11-09 22:02:38 +0100250 function (e) {
Akronff1b1f32020-10-18 11:41:29 +0200251 src = g.node(e.v);
252 target = g.node(e.w);
253 p = that._c('path');
Akronc56cf2d2016-11-09 22:02:38 +0100254 p.setAttributeNS(null, "d", _line(src, target));
255 p.classList.add('edge');
256 canvas.appendChild(p);
Akronff1b1f32020-10-18 11:41:29 +0200257 }
258 );
259
260 let height = g.graph().height;
Nils Diewald0e6992a2015-04-14 20:13:52 +0000261
262 // Create nodes
263 g.nodes().forEach(
Akronc56cf2d2016-11-09 22:02:38 +0100264 function (v) {
265 v = g.node(v);
Akronff1b1f32020-10-18 11:41:29 +0200266 const group = that._c('g');
Akronc56cf2d2016-11-09 22:02:38 +0100267 group.setAttribute('class', v.class);
268
269 // Add node box
Akronff1b1f32020-10-18 11:41:29 +0200270 const rect = group.appendChild(that._c('rect'));
Akronc56cf2d2016-11-09 22:02:38 +0100271 rect.setAttribute('x', v.x - v.width / 2);
272 rect.setAttribute('y', v.y - v.height / 2);
273 rect.setAttribute('rx', 5);
274 rect.setAttribute('ry', 5);
275 rect.setAttribute('width', v.width);
276 rect.setAttribute('height', v.height);
Nils Diewald0e6992a2015-04-14 20:13:52 +0000277
Akronc56cf2d2016-11-09 22:02:38 +0100278 if (v.class === 'root' && v.label === undefined) {
279 rect.setAttribute('width', v.height);
280 rect.setAttribute('x', v.x - v.height / 2);
281 rect.setAttribute('class', 'empty');
282 };
Nils Diewald0e6992a2015-04-14 20:13:52 +0000283
Akronc56cf2d2016-11-09 22:02:38 +0100284 // Add label
285 if (v.label !== undefined) {
Akronff1b1f32020-10-18 11:41:29 +0200286 const text = group.appendChild(that._c('text'));
287 let y = v.y - v.height / 2;
Akronc56cf2d2016-11-09 22:02:38 +0100288 text.setAttribute('y', y);
289 text.setAttribute(
290 'transform',
291 'translate(' + v.width/2 + ',' + ((v.height / 2) + 5) + ')'
292 );
Akron3bdac532019-03-04 13:24:43 +0100293
Akronff1b1f32020-10-18 11:41:29 +0200294 const vLabel = v.label.replace(/ /g, " ")
295 .replace(/&/g, '&')
296 .replace(/&lt;/g, '<')
297 .replace(/&gt;/g, '>');
Akronc56cf2d2016-11-09 22:02:38 +0100298
299 if (v.class === "leaf") {
Akron3bdac532019-03-04 13:24:43 +0100300 text.setAttribute('title', vLabel);
Nils Diewald0e6992a2015-04-14 20:13:52 +0000301
Akronb50964a2020-10-12 11:44:37 +0200302 let n = 0;
303 let tspan;
304 vLabel.split(" ").forEach(function(p) {
305 if (p.length === 0)
306 return;
Nils Diewald0e6992a2015-04-14 20:13:52 +0000307
Akronb50964a2020-10-12 11:44:37 +0200308 tspan = that._c('tspan');
309 tspan.appendChild(d.createTextNode(p));
Akronff1b1f32020-10-18 11:41:29 +0200310
Akronc56cf2d2016-11-09 22:02:38 +0100311 if (n !== 0)
312 tspan.setAttribute('dy', LINEHEIGHT + 'pt');
313 else
314 n = 1;
Akronff1b1f32020-10-18 11:41:29 +0200315
Akronc56cf2d2016-11-09 22:02:38 +0100316 tspan.setAttribute('x', v.x - v.width / 2);
317 y += LINEHEIGHT;
318 text.appendChild(tspan);
Akronb50964a2020-10-12 11:44:37 +0200319 });
Nils Diewald0e6992a2015-04-14 20:13:52 +0000320
Akronc56cf2d2016-11-09 22:02:38 +0100321 y += LINEHEIGHT;
Nils Diewald4347ee92015-05-04 20:32:48 +0000322
Akronc56cf2d2016-11-09 22:02:38 +0100323 // The text is below the canvas - readjust the height!
324 if (y > height)
325 height = y;
326 }
327 else {
Akronff1b1f32020-10-18 11:41:29 +0200328 const tspan = that._c('tspan');
Akron3bdac532019-03-04 13:24:43 +0100329 tspan.appendChild(d.createTextNode(vLabel));
Akronc56cf2d2016-11-09 22:02:38 +0100330 tspan.setAttribute('x', v.x - v.width / 2);
331 text.appendChild(tspan);
332 };
333 };
334 canvas.appendChild(group);
335 }
Nils Diewald0e6992a2015-04-14 20:13:52 +0000336 );
337
Nils Diewald4347ee92015-05-04 20:32:48 +0000338 canvas.setAttribute('width', g.graph().width);
339 canvas.setAttribute('height', height);
Nils Diewald0e6992a2015-04-14 20:13:52 +0000340 return this._element;
Akron151bc872018-02-02 14:04:15 +0100341 },
Akronff1b1f32020-10-18 11:41:29 +0200342
Akron151bc872018-02-02 14:04:15 +0100343 downloadLink : function () {
Akronff1b1f32020-10-18 11:41:29 +0200344 const a = d.createElement('a');
Akron151bc872018-02-02 14:04:15 +0100345 a.setAttribute('href-lang', 'image/svg+xml');
346 a.setAttribute('href', 'data:image/svg+xml;base64,' + this.toBase64());
347 a.setAttribute('download', 'tree.svg');
348 a.target = '_blank';
349 a.setAttribute('rel', 'noopener noreferrer');
350 return a;
Nils Diewald0e6992a2015-04-14 20:13:52 +0000351 }
352 };
353});