blob: c17fe20cb6406e29d27514c6c4a9046ddadf6b6c [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
10 var svgXmlns = "http://www.w3.org/2000/svg";
11 var _TermRE = new RegExp("^(?:([^\/]+?)\/)?([^:]+?):(.+?)$");
12
Nils Diewald7148c6f2015-05-04 15:07:53 +000013 // Node size
Nils Diewald4347ee92015-05-04 20:32:48 +000014 var WIDTH = 55, HEIGHT = 20, LINEHEIGHT = 14;
Nils Diewald7148c6f2015-05-04 15:07:53 +000015
Nils Diewald0e6992a2015-04-14 20:13:52 +000016 // Create path for node connections
17 function _line (src, target) {
18 var x1 = src.x,
19 y1 = src.y,
20 x2 = target.x,
21 y2 = target.y - target.height / 2;
22
23 // c 0,0 -10,0
24 return 'M ' + x1 + ',' + y1 + ' ' +
25 'C ' + x1 + ',' + y1 + ' ' +
26 x2 + ',' + (y2 - (y2 - y1) / 2) + ' ' +
27 x2 + ',' + y2;
28 };
29
30 return {
Nils Diewald7148c6f2015-05-04 15:07:53 +000031
32 /**
33 * Create new tree visualization based
34 * on a match snippet.
35 */
Nils Diewald0e6992a2015-04-14 20:13:52 +000036 create : function (snippet) {
Nils Diewald7148c6f2015-05-04 15:07:53 +000037 return Object.create(this).
Akronc56cf2d2016-11-09 22:02:38 +010038 _init(snippet);
Nils Diewald0e6992a2015-04-14 20:13:52 +000039 },
40
Nils Diewald0e6992a2015-04-14 20:13:52 +000041
Nils Diewald7148c6f2015-05-04 15:07:53 +000042 // Initialize the tree based on a snippet.
Nils Diewald0e6992a2015-04-14 20:13:52 +000043 _init : function (snippet) {
44 this._next = new Number(0);
Akronc56cf2d2016-11-09 22:02:38 +010045
Nils Diewald0e6992a2015-04-14 20:13:52 +000046 // Create html for traversal
47 var html = document.createElement("div");
48 html.innerHTML = snippet;
49 var g = new dagre.graphlib.Graph({
Akronc56cf2d2016-11-09 22:02:38 +010050 "directed" : true
Nils Diewald0e6992a2015-04-14 20:13:52 +000051 });
52 g.setGraph({
Akronc56cf2d2016-11-09 22:02:38 +010053 "nodesep" : 35,
54 "ranksep" : 15,
55 "marginx" : 40,
56 "marginy" : 10
Nils Diewald0e6992a2015-04-14 20:13:52 +000057 });
58 g.setDefaultEdgeLabel({});
59
60 this._graph = g;
61
62 // This is a new root
63 this._addNode(
Akronc56cf2d2016-11-09 22:02:38 +010064 this._next++,
65 { "class" : "root" }
Nils Diewald0e6992a2015-04-14 20:13:52 +000066 );
67
68 // Parse nodes from root
Akron98a933f2016-08-11 00:19:17 +020069 this._parse(0, html.childNodes, undefined);
Nils Diewald0e6992a2015-04-14 20:13:52 +000070
71 // Root node has only one child - remove
72 if (g.outEdges(0).length === 1)
Akronc56cf2d2016-11-09 22:02:38 +010073 g.removeNode(0);
Nils Diewald0e6992a2015-04-14 20:13:52 +000074
75 html = undefined;
76 return this;
77 },
78
Nils Diewald7148c6f2015-05-04 15:07:53 +000079 /**
80 * The number of nodes in the tree.
81 */
82 nodes : function () {
83 return this._next;
84 },
85
86 // Add new node to graph
87 _addNode : function (id, obj) {
88 obj["width"] = WIDTH;
89 obj["height"] = HEIGHT;
90 this._graph.setNode(id, obj)
Akron98a933f2016-08-11 00:19:17 +020091 return obj;
Nils Diewald7148c6f2015-05-04 15:07:53 +000092 },
93
94 // Add new edge to graph
95 _addEdge : function (src, target) {
96 this._graph.setEdge(src, target);
97 },
98
Nils Diewald0e6992a2015-04-14 20:13:52 +000099 // Remove foundry and layer for labels
100 _clean : function (title) {
101 return title.replace(_TermRE, "$3");
102 },
103
104 // Parse the snippet
Akron98a933f2016-08-11 00:19:17 +0200105 _parse : function (parent, children, mark) {
Nils Diewald0e6992a2015-04-14 20:13:52 +0000106 for (var i in children) {
Akronc56cf2d2016-11-09 22:02:38 +0100107 var c = children[i];
Nils Diewald0e6992a2015-04-14 20:13:52 +0000108
Akronc56cf2d2016-11-09 22:02:38 +0100109 // Element node
110 if (c.nodeType == 1) {
Nils Diewald0e6992a2015-04-14 20:13:52 +0000111
Akronc56cf2d2016-11-09 22:02:38 +0100112 // Get title from html
113 if (c.getAttribute("title")) {
114 var title = this._clean(c.getAttribute("title"));
Nils Diewald0e6992a2015-04-14 20:13:52 +0000115
Akronc56cf2d2016-11-09 22:02:38 +0100116 // Add child node
117 var id = this._next++;
Nils Diewald0e6992a2015-04-14 20:13:52 +0000118
Akronc56cf2d2016-11-09 22:02:38 +0100119 var obj = this._addNode(id, {
120 "class" : "middle",
121 "label" : title
122 });
Akron98a933f2016-08-11 00:19:17 +0200123
124 if (mark !== undefined) {
125 obj.class += ' mark';
126 };
127
Akronc56cf2d2016-11-09 22:02:38 +0100128 this._addEdge(parent, id);
Nils Diewald0e6992a2015-04-14 20:13:52 +0000129
Akronc56cf2d2016-11-09 22:02:38 +0100130 // Check for next level
131 if (c.hasChildNodes())
132 this._parse(id, c.childNodes, mark);
133 }
Nils Diewald0e6992a2015-04-14 20:13:52 +0000134
Akronc56cf2d2016-11-09 22:02:38 +0100135 // Step further
136 else if (c.hasChildNodes()) {
Nils Diewald0e6992a2015-04-14 20:13:52 +0000137
Akron98a933f2016-08-11 00:19:17 +0200138 if (c.tagName === 'MARK') {
Akronc56cf2d2016-11-09 22:02:38 +0100139 this._parse(parent, c.childNodes, true);
Akron98a933f2016-08-11 00:19:17 +0200140 }
141 else {
Akronc56cf2d2016-11-09 22:02:38 +0100142 this._parse(parent, c.childNodes, mark);
Akron98a933f2016-08-11 00:19:17 +0200143 };
144 };
Akronc56cf2d2016-11-09 22:02:38 +0100145 }
Nils Diewald0e6992a2015-04-14 20:13:52 +0000146
Akronc56cf2d2016-11-09 22:02:38 +0100147 // Text node
148 else if (c.nodeType == 3)
Nils Diewald0e6992a2015-04-14 20:13:52 +0000149
Akronc56cf2d2016-11-09 22:02:38 +0100150 if (c.nodeValue.match(/[-a-z0-9]/i)) {
Nils Diewald0e6992a2015-04-14 20:13:52 +0000151
Akronc56cf2d2016-11-09 22:02:38 +0100152 // Add child node
153 var id = this._next++;
154 this._addNode(id, {
155 "class" : "leaf",
156 "label" : c.nodeValue
157 });
Akron98a933f2016-08-11 00:19:17 +0200158
Akronc56cf2d2016-11-09 22:02:38 +0100159 this._addEdge(parent, id);
160 };
Nils Diewald0e6992a2015-04-14 20:13:52 +0000161 };
162 return this;
163 },
164
Akron0988d882017-11-10 16:13:12 +0100165 // Dummy method to be compatible with relTree
166 show : function () {
167 return;
168 },
169
Nils Diewald0e6992a2015-04-14 20:13:52 +0000170 /**
171 * Center the viewport of the canvas
Akron0988d882017-11-10 16:13:12 +0100172 * TODO:
173 * This is identical to relations
Nils Diewald0e6992a2015-04-14 20:13:52 +0000174 */
175 center : function () {
176 if (this._element === undefined)
Akronc56cf2d2016-11-09 22:02:38 +0100177 return;
Nils Diewald0e6992a2015-04-14 20:13:52 +0000178
179 var treeDiv = this._element.parentNode;
180
181 var cWidth = parseFloat(window.getComputedStyle(this._element).width);
182 var treeWidth = parseFloat(window.getComputedStyle(treeDiv).width);
183 // Reposition:
184 if (cWidth > treeWidth) {
Akronc56cf2d2016-11-09 22:02:38 +0100185 var scrollValue = (cWidth - treeWidth) / 2;
186 treeDiv.scrollLeft = scrollValue;
Nils Diewald0e6992a2015-04-14 20:13:52 +0000187 };
188 },
189
Akronc56cf2d2016-11-09 22:02:38 +0100190
Akron151bc872018-02-02 14:04:15 +0100191 /**
192 * Create svg and serialize as base64
193 */
Akronc56cf2d2016-11-09 22:02:38 +0100194 toBase64 : function () {
Akron6f32f822016-11-10 00:23:40 +0100195
196 // First clone element
197 var svgWrapper = document.createElement('div')
198 svgWrapper.innerHTML = this.element().outerHTML;
199 var svg = svgWrapper.firstChild;
200
201 var style = document.createElementNS(svgXmlns, 'style');
202 svg.getElementsByTagName('defs')[0].appendChild(style);
203
204 style.innerHTML =
205 'path.edge ' + '{ stroke: black; stroke-width: 2pt; fill: none; }' +
206 'g.root rect.empty,' +
207 'g.middle rect' + '{ stroke: black; stroke-width: 2pt; fill: #bbb; }' +
208 'g.leaf > rect ' + '{ display: none }' +
209 'g > text > tspan ' + '{ text-anchor: middle; font-size: 9pt }' +
210 'g.leaf > text > tspan ' + '{ font-size: 10pt; overflow: visible; }';
211
212 return btoa(unescape(encodeURIComponent(svg.outerHTML)));
Akronc56cf2d2016-11-09 22:02:38 +0100213 },
214
Nils Diewald7148c6f2015-05-04 15:07:53 +0000215 /**
216 * Get the dom element of the tree view.
217 */
Nils Diewald0e6992a2015-04-14 20:13:52 +0000218 element : function () {
219 if (this._element !== undefined)
Akronc56cf2d2016-11-09 22:02:38 +0100220 return this._element;
Nils Diewald0e6992a2015-04-14 20:13:52 +0000221
222 var g = this._graph;
223
224 dagre.layout(g);
Akronc56cf2d2016-11-09 22:02:38 +0100225
Nils Diewald0e6992a2015-04-14 20:13:52 +0000226 var canvas = document.createElementNS(svgXmlns, 'svg');
227 this._element = canvas;
228
Akron6f32f822016-11-10 00:23:40 +0100229 canvas.appendChild(document.createElementNS(svgXmlns, 'defs'));
230
Nils Diewald4347ee92015-05-04 20:32:48 +0000231 var height = g.graph().height;
Nils Diewald0e6992a2015-04-14 20:13:52 +0000232
233 // Create edges
234 g.edges().forEach(
Akronc56cf2d2016-11-09 22:02:38 +0100235 function (e) {
236 var src = g.node(e.v);
237 var target = g.node(e.w);
238 var p = document.createElementNS(svgXmlns, 'path');
239 p.setAttributeNS(null, "d", _line(src, target));
240 p.classList.add('edge');
241 canvas.appendChild(p);
242 });
Nils Diewald0e6992a2015-04-14 20:13:52 +0000243
244 // Create nodes
245 g.nodes().forEach(
Akronc56cf2d2016-11-09 22:02:38 +0100246 function (v) {
247 v = g.node(v);
248 var group = document.createElementNS(svgXmlns, 'g');
249 group.setAttribute('class', v.class);
250
251 // Add node box
252 var rect = group.appendChild(document.createElementNS(svgXmlns, 'rect'));
253 rect.setAttribute('x', v.x - v.width / 2);
254 rect.setAttribute('y', v.y - v.height / 2);
255 rect.setAttribute('rx', 5);
256 rect.setAttribute('ry', 5);
257 rect.setAttribute('width', v.width);
258 rect.setAttribute('height', v.height);
Nils Diewald0e6992a2015-04-14 20:13:52 +0000259
Akronc56cf2d2016-11-09 22:02:38 +0100260 if (v.class === 'root' && v.label === undefined) {
261 rect.setAttribute('width', v.height);
262 rect.setAttribute('x', v.x - v.height / 2);
263 rect.setAttribute('class', 'empty');
264 };
Nils Diewald0e6992a2015-04-14 20:13:52 +0000265
Akronc56cf2d2016-11-09 22:02:38 +0100266 // Add label
267 if (v.label !== undefined) {
268 var text = group.appendChild(document.createElementNS(svgXmlns, 'text'));
269 var y = v.y - v.height / 2;
270 text.setAttribute('y', y);
271 text.setAttribute(
272 'transform',
273 'translate(' + v.width/2 + ',' + ((v.height / 2) + 5) + ')'
274 );
275
276 if (v.class === "leaf") {
277 text.setAttribute('title', v.label);
Nils Diewald0e6992a2015-04-14 20:13:52 +0000278
Akronc56cf2d2016-11-09 22:02:38 +0100279 var labelPart = v.label.split(" ");
280 var n = 0;
281 for (var i = 0; i < labelPart.length; i++) {
282 if (labelPart[i].length === 0)
283 continue;
Nils Diewald0e6992a2015-04-14 20:13:52 +0000284
Akronc56cf2d2016-11-09 22:02:38 +0100285 var tspan = document.createElementNS(svgXmlns, 'tspan');
286 tspan.appendChild(document.createTextNode(labelPart[i]));
287 if (n !== 0)
288 tspan.setAttribute('dy', LINEHEIGHT + 'pt');
289 else
290 n = 1;
291 tspan.setAttribute('x', v.x - v.width / 2);
292 y += LINEHEIGHT;
293 text.appendChild(tspan);
294 };
Nils Diewald0e6992a2015-04-14 20:13:52 +0000295
Akronc56cf2d2016-11-09 22:02:38 +0100296 y += LINEHEIGHT;
Nils Diewald4347ee92015-05-04 20:32:48 +0000297
Akronc56cf2d2016-11-09 22:02:38 +0100298 // The text is below the canvas - readjust the height!
299 if (y > height)
300 height = y;
301 }
302 else {
303 var tspan = document.createElementNS(svgXmlns, 'tspan');
304 tspan.appendChild(document.createTextNode(v.label));
305 tspan.setAttribute('x', v.x - v.width / 2);
306 text.appendChild(tspan);
307 };
308 };
309 canvas.appendChild(group);
310 }
Nils Diewald0e6992a2015-04-14 20:13:52 +0000311 );
312
Nils Diewald4347ee92015-05-04 20:32:48 +0000313 canvas.setAttribute('width', g.graph().width);
314 canvas.setAttribute('height', height);
Nils Diewald0e6992a2015-04-14 20:13:52 +0000315 return this._element;
Akron151bc872018-02-02 14:04:15 +0100316 },
317
318 downloadLink : function () {
319 var a = document.createElement('a');
320 a.setAttribute('href-lang', 'image/svg+xml');
321 a.setAttribute('href', 'data:image/svg+xml;base64,' + this.toBase64());
322 a.setAttribute('download', 'tree.svg');
323 a.target = '_blank';
324 a.setAttribute('rel', 'noopener noreferrer');
325 return a;
Nils Diewald0e6992a2015-04-14 20:13:52 +0000326 }
327 };
328});