blob: a52c883aab3684498fb56da7f1e6391f1d9a463d [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 */
Akrone51eaa32020-11-10 09:35:53 +01007"use strict";
8
Nils Diewald0e6992a2015-04-14 20:13:52 +00009define(['lib/dagre'], function (dagre) {
Nils Diewald0e6992a2015-04-14 20:13:52 +000010
Akron0b489ad2018-02-02 16:49:32 +010011 const d = document;
12 const svgNS = "http://www.w3.org/2000/svg";
13 const _TermRE = new RegExp("^(?:([^\/]+?)\/)?([^:]+?):(.+?)$");
Nils Diewald0e6992a2015-04-14 20:13:52 +000014
Nils Diewald7148c6f2015-05-04 15:07:53 +000015 // Node size
Nils Diewald4347ee92015-05-04 20:32:48 +000016 var WIDTH = 55, HEIGHT = 20, LINEHEIGHT = 14;
Nils Diewald7148c6f2015-05-04 15:07:53 +000017
Nils Diewald0e6992a2015-04-14 20:13:52 +000018 // Create path for node connections
19 function _line (src, target) {
Akronff1b1f32020-10-18 11:41:29 +020020 const x1 = src.x,
21 y1 = src.y,
22 x2 = target.x,
23 y2 = target.y - target.height / 2;
Nils Diewald0e6992a2015-04-14 20:13:52 +000024
25 // c 0,0 -10,0
26 return 'M ' + x1 + ',' + y1 + ' ' +
27 'C ' + x1 + ',' + y1 + ' ' +
28 x2 + ',' + (y2 - (y2 - y1) / 2) + ' ' +
29 x2 + ',' + y2;
30 };
31
32 return {
Nils Diewald7148c6f2015-05-04 15:07:53 +000033
34 /**
35 * Create new tree visualization based
36 * on a match snippet.
37 */
Nils Diewald0e6992a2015-04-14 20:13:52 +000038 create : function (snippet) {
Nils Diewald7148c6f2015-05-04 15:07:53 +000039 return Object.create(this).
Akronff1b1f32020-10-18 11:41:29 +020040 _init(snippet);
Nils Diewald0e6992a2015-04-14 20:13:52 +000041 },
42
Nils Diewald0e6992a2015-04-14 20:13:52 +000043
Nils Diewald7148c6f2015-05-04 15:07:53 +000044 // Initialize the tree based on a snippet.
Nils Diewald0e6992a2015-04-14 20:13:52 +000045 _init : function (snippet) {
46 this._next = new Number(0);
Akronc56cf2d2016-11-09 22:02:38 +010047
Nils Diewald0e6992a2015-04-14 20:13:52 +000048 // Create html for traversal
Akronff1b1f32020-10-18 11:41:29 +020049 let html = d.createElement("div");
Nils Diewald0e6992a2015-04-14 20:13:52 +000050 html.innerHTML = snippet;
Akronff1b1f32020-10-18 11:41:29 +020051
52 const g = new dagre.graphlib.Graph({
53 "directed" : true
Nils Diewald0e6992a2015-04-14 20:13:52 +000054 });
Akronff1b1f32020-10-18 11:41:29 +020055
Nils Diewald0e6992a2015-04-14 20:13:52 +000056 g.setGraph({
Akronff1b1f32020-10-18 11:41:29 +020057 "nodesep" : 35,
58 "ranksep" : 15,
59 "marginx" : 40,
60 "marginy" : 10
Nils Diewald0e6992a2015-04-14 20:13:52 +000061 });
62 g.setDefaultEdgeLabel({});
63
64 this._graph = g;
65
66 // This is a new root
67 this._addNode(
Akronff1b1f32020-10-18 11:41:29 +020068 this._next++,
69 { "class" : "root" }
Nils Diewald0e6992a2015-04-14 20:13:52 +000070 );
71
72 // Parse nodes from root
Akron98a933f2016-08-11 00:19:17 +020073 this._parse(0, html.childNodes, undefined);
Nils Diewald0e6992a2015-04-14 20:13:52 +000074
75 // Root node has only one child - remove
76 if (g.outEdges(0).length === 1)
Akronff1b1f32020-10-18 11:41:29 +020077 g.removeNode(0);
Nils Diewald0e6992a2015-04-14 20:13:52 +000078
79 html = undefined;
80 return this;
81 },
82
Akron0b489ad2018-02-02 16:49:32 +010083
84 _c : function (tag) {
85 return d.createElementNS(svgNS, tag);
86 },
87
Akronff1b1f32020-10-18 11:41:29 +020088
Nils Diewald7148c6f2015-05-04 15:07:53 +000089 /**
90 * The number of nodes in the tree.
91 */
92 nodes : function () {
93 return this._next;
94 },
95
Akronff1b1f32020-10-18 11:41:29 +020096
Nils Diewald7148c6f2015-05-04 15:07:53 +000097 // Add new node to graph
98 _addNode : function (id, obj) {
99 obj["width"] = WIDTH;
100 obj["height"] = HEIGHT;
101 this._graph.setNode(id, obj)
Akron98a933f2016-08-11 00:19:17 +0200102 return obj;
Nils Diewald7148c6f2015-05-04 15:07:53 +0000103 },
104
Akronff1b1f32020-10-18 11:41:29 +0200105
Nils Diewald7148c6f2015-05-04 15:07:53 +0000106 // Add new edge to graph
107 _addEdge : function (src, target) {
108 this._graph.setEdge(src, target);
109 },
110
Akronff1b1f32020-10-18 11:41:29 +0200111
Nils Diewald0e6992a2015-04-14 20:13:52 +0000112 // Remove foundry and layer for labels
113 _clean : function (title) {
114 return title.replace(_TermRE, "$3");
115 },
116
Akronff1b1f32020-10-18 11:41:29 +0200117
Nils Diewald0e6992a2015-04-14 20:13:52 +0000118 // Parse the snippet
Akron98a933f2016-08-11 00:19:17 +0200119 _parse : function (parent, children, mark) {
Akron678c26f2020-10-09 08:52:50 +0200120 children.forEach(function(c) {
Nils Diewald0e6992a2015-04-14 20:13:52 +0000121
Akronff1b1f32020-10-18 11:41:29 +0200122 // Element node
123 if (c.nodeType == 1) {
Nils Diewald0e6992a2015-04-14 20:13:52 +0000124
Akronff1b1f32020-10-18 11:41:29 +0200125 // Get title from html
126 if (c.getAttribute("title")) {
Nils Diewald0e6992a2015-04-14 20:13:52 +0000127
Akronff1b1f32020-10-18 11:41:29 +0200128 // Add child node
129 const id = this._next++;
Nils Diewald0e6992a2015-04-14 20:13:52 +0000130
Akronff1b1f32020-10-18 11:41:29 +0200131 const obj = this._addNode(id, {
132 "class" : "middle",
133 "label" : this._clean(c.getAttribute("title"))
134 });
Akron98a933f2016-08-11 00:19:17 +0200135
136 if (mark !== undefined) {
137 obj.class += ' mark';
138 };
139
Akronff1b1f32020-10-18 11:41:29 +0200140 this._addEdge(parent, id);
Nils Diewald0e6992a2015-04-14 20:13:52 +0000141
Akronff1b1f32020-10-18 11:41:29 +0200142 // Check for next level
143 if (c.hasChildNodes())
144 this._parse(id, c.childNodes, mark);
145 }
Nils Diewald0e6992a2015-04-14 20:13:52 +0000146
Akronff1b1f32020-10-18 11:41:29 +0200147 // Step further
148 else if (c.hasChildNodes()) {
Nils Diewald0e6992a2015-04-14 20:13:52 +0000149
Akronff1b1f32020-10-18 11:41:29 +0200150 this._parse(
151 parent,
152 c.childNodes,
153 c.tagName === 'MARK' ? true : mark
154 );
Akron98a933f2016-08-11 00:19:17 +0200155 };
Akronff1b1f32020-10-18 11:41:29 +0200156 }
Nils Diewald0e6992a2015-04-14 20:13:52 +0000157
Akronff1b1f32020-10-18 11:41:29 +0200158 // Text node
159 else if (c.nodeType == 3)
Nils Diewald0e6992a2015-04-14 20:13:52 +0000160
Akronff1b1f32020-10-18 11:41:29 +0200161 if (c.nodeValue.match(/[-a-z0-9]/i)) {
162
163 // Add child node
164 const id = this._next++;
165 this._addNode(id, {
166 "class" : "leaf",
167 "label" : c.nodeValue
168 });
Nils Diewald0e6992a2015-04-14 20:13:52 +0000169
Akronff1b1f32020-10-18 11:41:29 +0200170 this._addEdge(parent, id);
171 };
Akron678c26f2020-10-09 08:52:50 +0200172 }, this);
Nils Diewald0e6992a2015-04-14 20:13:52 +0000173 return this;
174 },
175
Akronff1b1f32020-10-18 11:41:29 +0200176
Akron0988d882017-11-10 16:13:12 +0100177 // Dummy method to be compatible with relTree
178 show : function () {
179 return;
180 },
181
Akronff1b1f32020-10-18 11:41:29 +0200182
Nils Diewald0e6992a2015-04-14 20:13:52 +0000183 /**
184 * Center the viewport of the canvas
Akron0988d882017-11-10 16:13:12 +0100185 * TODO:
186 * This is identical to relations
Nils Diewald0e6992a2015-04-14 20:13:52 +0000187 */
188 center : function () {
Akron24aa0052020-11-10 11:00:34 +0100189 if (this._el === undefined)
Akronff1b1f32020-10-18 11:41:29 +0200190 return;
Nils Diewald0e6992a2015-04-14 20:13:52 +0000191
Akron24aa0052020-11-10 11:00:34 +0100192 const treeDiv = this._el.parentNode;
193 const cWidth = parseFloat(window.getComputedStyle(this._el).width);
Akronff1b1f32020-10-18 11:41:29 +0200194 const treeWidth = parseFloat(window.getComputedStyle(treeDiv).width);
Nils Diewald0e6992a2015-04-14 20:13:52 +0000195
Nils Diewald0e6992a2015-04-14 20:13:52 +0000196 // Reposition:
197 if (cWidth > treeWidth) {
Akronff1b1f32020-10-18 11:41:29 +0200198 treeDiv.scrollLeft = (cWidth - treeWidth) / 2;
Nils Diewald0e6992a2015-04-14 20:13:52 +0000199 };
200 },
201
Akronc56cf2d2016-11-09 22:02:38 +0100202
Akron151bc872018-02-02 14:04:15 +0100203 /**
204 * Create svg and serialize as base64
205 */
Akronc56cf2d2016-11-09 22:02:38 +0100206 toBase64 : function () {
Akron6f32f822016-11-10 00:23:40 +0100207
208 // First clone element
Akronff1b1f32020-10-18 11:41:29 +0200209 const svgWrapper = d.createElement('div')
Akron6f32f822016-11-10 00:23:40 +0100210 svgWrapper.innerHTML = this.element().outerHTML;
Akron6f32f822016-11-10 00:23:40 +0100211
Akronff1b1f32020-10-18 11:41:29 +0200212 const svg = svgWrapper.firstChild;
213 const style = this._c('style');
214
Akron6f32f822016-11-10 00:23:40 +0100215 svg.getElementsByTagName('defs')[0].appendChild(style);
216
217 style.innerHTML =
218 'path.edge ' + '{ stroke: black; stroke-width: 2pt; fill: none; }' +
219 'g.root rect.empty,' +
220 'g.middle rect' + '{ stroke: black; stroke-width: 2pt; fill: #bbb; }' +
221 'g.leaf > rect ' + '{ display: none }' +
222 'g > text > tspan ' + '{ text-anchor: middle; font-size: 9pt }' +
223 'g.leaf > text > tspan ' + '{ font-size: 10pt; overflow: visible; }';
Akron3bdac532019-03-04 13:24:43 +0100224
225 return btoa(unescape(encodeURIComponent(svg.outerHTML)).replace(/ /g, ' '));
Akronc56cf2d2016-11-09 22:02:38 +0100226 },
227
Akronff1b1f32020-10-18 11:41:29 +0200228
Nils Diewald7148c6f2015-05-04 15:07:53 +0000229 /**
230 * Get the dom element of the tree view.
231 */
Nils Diewald0e6992a2015-04-14 20:13:52 +0000232 element : function () {
Akronff1b1f32020-10-18 11:41:29 +0200233
Akron24aa0052020-11-10 11:00:34 +0100234 if (this._el !== undefined)
235 return this._el;
Nils Diewald0e6992a2015-04-14 20:13:52 +0000236
Akronff1b1f32020-10-18 11:41:29 +0200237 const g = this._graph;
Nils Diewald0e6992a2015-04-14 20:13:52 +0000238 dagre.layout(g);
Akronc56cf2d2016-11-09 22:02:38 +0100239
Akronff1b1f32020-10-18 11:41:29 +0200240 const canvas = this._c('svg');
Akron24aa0052020-11-10 11:00:34 +0100241 this._el = canvas;
Nils Diewald0e6992a2015-04-14 20:13:52 +0000242
Akron0b489ad2018-02-02 16:49:32 +0100243 canvas.appendChild(this._c('defs'));
Nils Diewald0e6992a2015-04-14 20:13:52 +0000244
245 // Create edges
Akronff1b1f32020-10-18 11:41:29 +0200246 const that = this;
247
248 let src, target, p;
249
Nils Diewald0e6992a2015-04-14 20:13:52 +0000250 g.edges().forEach(
Akronc56cf2d2016-11-09 22:02:38 +0100251 function (e) {
Akronff1b1f32020-10-18 11:41:29 +0200252 src = g.node(e.v);
253 target = g.node(e.w);
254 p = that._c('path');
Akronc56cf2d2016-11-09 22:02:38 +0100255 p.setAttributeNS(null, "d", _line(src, target));
256 p.classList.add('edge');
257 canvas.appendChild(p);
Akronff1b1f32020-10-18 11:41:29 +0200258 }
259 );
260
261 let height = g.graph().height;
Nils Diewald0e6992a2015-04-14 20:13:52 +0000262
263 // Create nodes
264 g.nodes().forEach(
Akronc56cf2d2016-11-09 22:02:38 +0100265 function (v) {
266 v = g.node(v);
Akronff1b1f32020-10-18 11:41:29 +0200267 const group = that._c('g');
Akronc56cf2d2016-11-09 22:02:38 +0100268 group.setAttribute('class', v.class);
269
270 // Add node box
Akronff1b1f32020-10-18 11:41:29 +0200271 const rect = group.appendChild(that._c('rect'));
Akronc56cf2d2016-11-09 22:02:38 +0100272 rect.setAttribute('x', v.x - v.width / 2);
273 rect.setAttribute('y', v.y - v.height / 2);
274 rect.setAttribute('rx', 5);
275 rect.setAttribute('ry', 5);
276 rect.setAttribute('width', v.width);
277 rect.setAttribute('height', v.height);
Nils Diewald0e6992a2015-04-14 20:13:52 +0000278
Akronc56cf2d2016-11-09 22:02:38 +0100279 if (v.class === 'root' && v.label === undefined) {
280 rect.setAttribute('width', v.height);
281 rect.setAttribute('x', v.x - v.height / 2);
282 rect.setAttribute('class', 'empty');
283 };
Nils Diewald0e6992a2015-04-14 20:13:52 +0000284
Akronc56cf2d2016-11-09 22:02:38 +0100285 // Add label
286 if (v.label !== undefined) {
Akronff1b1f32020-10-18 11:41:29 +0200287 const text = group.appendChild(that._c('text'));
288 let y = v.y - v.height / 2;
Akronc56cf2d2016-11-09 22:02:38 +0100289 text.setAttribute('y', y);
290 text.setAttribute(
291 'transform',
292 'translate(' + v.width/2 + ',' + ((v.height / 2) + 5) + ')'
293 );
Akron3bdac532019-03-04 13:24:43 +0100294
Akronff1b1f32020-10-18 11:41:29 +0200295 const vLabel = v.label.replace(/ /g, " ")
296 .replace(/&/g, '&')
297 .replace(/&lt;/g, '<')
298 .replace(/&gt;/g, '>');
Akronc56cf2d2016-11-09 22:02:38 +0100299
300 if (v.class === "leaf") {
Akron3bdac532019-03-04 13:24:43 +0100301 text.setAttribute('title', vLabel);
Nils Diewald0e6992a2015-04-14 20:13:52 +0000302
Akronb50964a2020-10-12 11:44:37 +0200303 let n = 0;
304 let tspan;
305 vLabel.split(" ").forEach(function(p) {
306 if (p.length === 0)
307 return;
Nils Diewald0e6992a2015-04-14 20:13:52 +0000308
Akronb50964a2020-10-12 11:44:37 +0200309 tspan = that._c('tspan');
310 tspan.appendChild(d.createTextNode(p));
Akronff1b1f32020-10-18 11:41:29 +0200311
Akronc56cf2d2016-11-09 22:02:38 +0100312 if (n !== 0)
313 tspan.setAttribute('dy', LINEHEIGHT + 'pt');
314 else
315 n = 1;
Akronff1b1f32020-10-18 11:41:29 +0200316
Akronc56cf2d2016-11-09 22:02:38 +0100317 tspan.setAttribute('x', v.x - v.width / 2);
318 y += LINEHEIGHT;
319 text.appendChild(tspan);
Akronb50964a2020-10-12 11:44:37 +0200320 });
Nils Diewald0e6992a2015-04-14 20:13:52 +0000321
Akronc56cf2d2016-11-09 22:02:38 +0100322 y += LINEHEIGHT;
Nils Diewald4347ee92015-05-04 20:32:48 +0000323
Akronc56cf2d2016-11-09 22:02:38 +0100324 // The text is below the canvas - readjust the height!
325 if (y > height)
326 height = y;
327 }
328 else {
Akronff1b1f32020-10-18 11:41:29 +0200329 const tspan = that._c('tspan');
Akron3bdac532019-03-04 13:24:43 +0100330 tspan.appendChild(d.createTextNode(vLabel));
Akronc56cf2d2016-11-09 22:02:38 +0100331 tspan.setAttribute('x', v.x - v.width / 2);
332 text.appendChild(tspan);
333 };
334 };
335 canvas.appendChild(group);
336 }
Nils Diewald0e6992a2015-04-14 20:13:52 +0000337 );
338
Nils Diewald4347ee92015-05-04 20:32:48 +0000339 canvas.setAttribute('width', g.graph().width);
340 canvas.setAttribute('height', height);
Akron24aa0052020-11-10 11:00:34 +0100341 return this._el;
Akron151bc872018-02-02 14:04:15 +0100342 },
Akronff1b1f32020-10-18 11:41:29 +0200343
Akron151bc872018-02-02 14:04:15 +0100344 downloadLink : function () {
Akronff1b1f32020-10-18 11:41:29 +0200345 const a = d.createElement('a');
Akron151bc872018-02-02 14:04:15 +0100346 a.setAttribute('href-lang', 'image/svg+xml');
347 a.setAttribute('href', 'data:image/svg+xml;base64,' + this.toBase64());
348 a.setAttribute('download', 'tree.svg');
349 a.target = '_blank';
350 a.setAttribute('rel', 'noopener noreferrer');
351 return a;
Nils Diewald0e6992a2015-04-14 20:13:52 +0000352 }
353 };
354});