/**
* Main function. Specifies transformations that are needed for creating a JSON out of XML.
* @constructor
* @param {string} divClass - id of element that will contain a tree.
* @param {string} file - Path to XML/JSON file.
* @param {array} impNodes - Important nodes that should be shown on the second level of the tree.
* @param {boolean} isAttributes - Whether to show or not all additional attributes.
* @param {boolean} isJSON - Specifies type of the input (JSON/XML).
*/
function xml2tree(divClass, file, impNodes, isAttributes, isJSON) {
if (isJSON) {
var newText = readTextFile(file);
var newObj = JSON.parse(newText);
var mapArray = JSONToArray(newObj);
var JSONText = arrayToJSON(mapArray); // converts array into a JSON file
if (newObj.results.bindings.length > 4) {
drawTree(divClass, JSONText, newObj.results.bindings.length * 2, 5);
} else {
drawTree(divClass, JSONText, 6, 5);
}
} else {
var XMLText = readTextFile(file);
var tagArray = XMLToArray(XMLText); // returns an array of all nodes with related info
var mapArray = arrayMapping(tagArray, impNodes, isAttributes)
var JSONText = arrayToJSON(mapArray); // converts array into a JSON file
var maxDepth = 0; // we evaluate the maxDepth of the tree in order to draw a frame for it
var maxWidth = 0; // we evaluate the maxWidth of the tree in order to draw a frame for it
var depthArray = [];
for (var i = 0; i <= tagArray.length; i++) {
depthArray.push(0);
}
for (var i = 0; i < tagArray.length; i++) {
if (tagArray[i].depth > maxWidth) {
maxWidth = tagArray[i].depth;
}
depthArray[tagArray[i].depth] += 1;
}
maxWidth += 1;
maxDepth = Math.max.apply(null, depthArray);
divClass = '.' + divClass;
drawTree(divClass, JSONText, maxDepth, maxWidth);
}
}
/**
* Reads and returns content of the input file.
* @constructor
* @param {string} file - Path to XML/JSON file.
*/
function readTextFile(file){
var allText = '';
var rawFile = new XMLHttpRequest();
rawFile.open('GET', file, false);
rawFile.onreadystatechange = function () {
if(rawFile.readyState === 4) {
if(rawFile.status === 200 || rawFile.status == 0) {
allText = rawFile.responseText;
}
}
}
rawFile.send(null);
return allText;
}
/**
* Creates an object out of information about the node
* @constructor
* @param {number} id - id of the node.
* @param {number} parent - id of node's parent.
* @param {array} children - ids of node's children.
* @param {number} depth - Depth at which the node is aligned.
* @param {string} name - Node name.
* @param {string} tag - Node tag.
* @param {string} type - Node type.
* @param {string} value - Node value.
* @param {string} extra - Extra information about the node.
*/
function objInit(id, parent, children, depth, name, tag, type, value, extra) { // object initializing
var obj = new Object();
obj.id = id;
obj.parent = parent;
obj.children = children;
obj.depth = depth;
obj.name = name;
obj.tag = tag;
obj.type = type;
obj.value = value;
obj.extra = extra;
return obj;
}
/**
* Creates an array out of JSON object.
* @constructor
* @param {object} newObj - JSON Object with input contents.
*/
function JSONToArray(newObj) { // creating an array of all objects that needed to be visualized
var totalIdNum = 1;
var mapArray = [];
var rootObj = new Object();
rootObj = objInit(totalIdNum, 0, [], 0, 'results', '', '', '', '');
mapArray.push(rootObj);
for (var i = 0; i < newObj.results.bindings.length; i++) {
var depth = 1;
if (newObj.results.bindings[i].s) { //subject
totalIdNum += 1;
rootObj.children.push(totalIdNum);
var subjectObj = new Object();
subjectObj = objInit(totalIdNum, 1, [], depth, 'subject', '', newObj.results.bindings[i].s.type, newObj.results.bindings[i].s.value, '');
mapArray.push(subjectObj);
depth += 1;
}
if (newObj.results.bindings[i].p) { // predicate
totalIdNum += 1;
subjectObj.children.push(totalIdNum);
var predicatObj = new Object();
predicatObj = objInit(totalIdNum, totalIdNum - 1, [], depth, 'predicate', '', newObj.results.bindings[i].p.type, newObj.results.bindings[i].p.value, '');
mapArray.push(predicatObj);
depth += 1;
}
if (newObj.results.bindings[i].o) { // object
totalIdNum += 1;
predicatObj.children.push(totalIdNum);
var objectObj = new Object();
objectObj = objInit(totalIdNum, totalIdNum - 1, [], depth, 'object', '', newObj.results.bindings[i].o.type, newObj.results.bindings[i].o.value, '');
mapArray.push(objectObj);
depth += 1;
}
}
return mapArray
}
/**
* Creates an array out of XML.
* @constructor
* @param {string} text - XML text with input contents.
*/
function XMLToArray(text) {
var head = '';
var currentString = '';
var documentType = '';
var ifCurrentStringIsHead = false;
var ifCurrentStringIsComment = false;
var ifCurrentStringIsTag = false;
var ifCurrentStringIsDocumentType = false;
var newTag = new Object();
var tagArray = [];
var tagStack = [];
var id = 0;
for (var i = 0; i < text.length; i++) { // for all symbols in the text
if (text[i] == '<' && !ifCurrentStringIsComment) { // if we found an open tag and we are not writing a comment at the moment
if (text[i + 1] == '?') { // if that's a headline
ifCurrentStringIsHead = true;
} else if (text[i + 1] == '!' && text[i + 2] == '-' && text[i + 3] == '-') { // if that's an end of a comment
ifCurrentStringIsComment = true;
} else if (text[i + 1] == '!') {
ifCurrentStringIsDocumentType = true;
} else {
if (tagStack.length) { // if tagStack is not empty
while (currentString[currentString.length - 1] == ' ') {
currentString = currentString.substring(0, currentString.length - 1);
}
tagArray[tagStack[tagStack.length - 1].id - 1].value = currentString; // add the information that's outside tags
currentString = '';
}
ifCurrentStringIsTag = true; // start the next tag
}
} else if (text[i] == '>') { // if we found a close tag
if (ifCurrentStringIsHead) { // if we are writing a headline
if (text[i - 1] == '?') {
ifCurrentStringIsHead = false;
} else {
//
}
} else if (ifCurrentStringIsComment) { // if we are writing a comment
if (text[i - 1] == '-' && text[i - 2] == '-') { // and that's the end of the comment
ifCurrentStringIsComment = false;
}
} else if (ifCurrentStringIsDocumentType) { // if we are writing the document type
ifCurrentStringIsDocumentType = false;
} else if (ifCurrentStringIsTag) { // if we are writing the information that's inside tags
if (text[i - 1] == '/') { // if the tag is closed inside itself
newTag.tag = currentString.substring(0, currentString.length - 1); // isolating the information inside the tag
currentString = '';
id += 1;
newTag.id = id;
newTag.children = [];
newTag.depth = tagStack.length; // adding the depth level of the tag
if (tagStack.length) { // if there is smth in the stack
newTag.parent = tagStack[tagStack.length - 1].id; // we save the last element of stack as current tag's parent
tagArray[tagStack[tagStack.length - 1].id - 1].children.push(newTag.id); // we add a current tag as a child to the last element
} else {
newTag.parent = 0;
}
tagArray.push(newTag);
} else {
if (currentString[0] == '/') { // if it is a closing tag
currentString = '';
tagStack.pop(); // delete this tag from the stack
} else {
newTag.tag = currentString;
currentString = '';
id += 1;
newTag.id = id;
newTag.children = [];
newTag.depth = tagStack.length; // adding the depth level of the tag
if (tagStack.length) { // if there is smth in the stack
newTag.parent = tagStack[tagStack.length - 1].id; // we save the last element of stack as current tag's parent
tagArray[tagStack[tagStack.length - 1].id - 1].children.push(newTag.id); // we add a current tag as a child to the last element
} else {
newTag.parent = 0;
}
tagArray.push(newTag);
tagStack.push(newTag);
}
}
ifCurrentStringIsTag = false;
newTag={};
}
} else if (ifCurrentStringIsHead) {
if (text[i] !== '?' && !(text[i] == '?' && text[i+1] == '>') && !(text[i] == '?' && text[i-1] == '<')) {
head += text[i];
} else {
//
}
} else if (ifCurrentStringIsComment) {
//
} else if (ifCurrentStringIsTag) {
currentString += text[i];
} else if (ifCurrentStringIsDocumentType) {
documentType += text[i];
} else { // avoid all the irrelevant symbols inside and outside the tags
if (text[i] !== '\n' && text[i] != '\r' && text[i] != '\t' && !(text[i] == ' ' && !currentString.length)) {
currentString += text[i];
} else {
//
}
}
}
if (tagStack.length) { // if after the end of the process, the stack is not empty - means number of opening tags is bigger than closing
console.error('XML file is not correct!');
}
return tagArray;
}
/**
* Parses and processes all the information inside the array with input contents.
* @constructor
* @param {array} tagArray - Array of nodes' tags.
* @param {array} impNodes - List of nodes that should be shown on the second level.
* @param {boolean} isAttributes - Whether to show or not all additional attributes.
*/
function arrayMapping(tagArray, impNodes, isAttributes) {
var mapArray = attrTrans(tagArray, isAttributes);
if (impNodes.length) {
var extra = new Object();
extra.children = [];
extra.depth = 1;
extra.id = tagArray.length+1;
extra.parent = 1;
extra.type = 'Extra';
tagArray.push(extra);
noMoreChildren = [];
var ifChild = false;
for (var i = 0; i < tagArray.length - 1; i++) { // length - 1 because we want to exclude newly added tag 'Extra'
if (tagArray[i].depth == 1) {
ifChild = false;
for (var j = 0; j < impNodes.length; j++) {
if (tagArray[i].type == impNodes[j]) {
ifChild = true;
break;
}
}
if (!ifChild) {
tagArray[i].parent = extra.id;
tagArray[tagArray.length - 1].children.push(tagArray[i].id);
noMoreChildren.push(tagArray[i].id);
}
}
}
newRootChildren = [];
for (var i = 0; i < tagArray[0].children.length; i++) {
var ifEqual = false;
for (var j = 0; j < noMoreChildren.length; j++) {
if (tagArray[0].children[i] == noMoreChildren[j]) {
ifEqual = true;
break;
}
}
if (!ifEqual) {
newRootChildren.push(tagArray[0].children[i]);
}
}
tagArray[0].children = newRootChildren;
tagArray[0].children.push(extra.id);
}
return mapArray;
}
/**
* Creates a final structure of an object to draw.
* @constructor
* @param {array} tagArray - Array of nodes' tags.
* @param {boolean} isAttributes - Whether to show or not all additional attributes.
*/
function attrTrans(tagArray, isAttributes) { // dealing with attributes of the objects
for (var i = 0; i < tagArray.length; i++) {
var tagString = tagArray[i].tag;
var nodeAttributes = tagString.split(" ");
tagArray[i].type = '';
tagArray[i].attr = [];
for (var j = 0; j < nodeAttributes.length; j++) {
if (j === 0) {
tagArray[i].type = nodeAttributes[j];
} else if (isAttributes) {
tagArray[i].attr.push(nodeAttributes[j]);
}
}
}
return tagArray;
}
/**
* Converts an array to JSON.
* @constructor
* @param {array} tagArray - Array of nodes' tags.
*/
function arrayToJSON(tagArray) { // converting array to json type
var JSONText = [];
var root = new Object();
root = objToJSON(tagArray, 0, false);
JSONText.push(root);
return JSONText
}
/**
* Converts an object to JSON.
* @constructor
* @param {array} tagArray - Array of nodes' tags.
* @param {array} id - id of the node.
* @param {array} parent - id of node's parent.
*/
function objToJSON(tagArray, id, parent) {
var node = new Object(); // we create an empty object and save there all the relevant information about the node
node.value = tagArray[id].value;
node.name = tagArray[id].name;
node.extra = tagArray[id].extra;
node.type = tagArray[id].type;
node.attr = tagArray[id].attr;
if (parent == false) {
node.parent = 'null'
} else {
node.parent = parent.name;
}
node.children = [];
for (var i = 0; i < tagArray[id].children.length; i++) { // for all children of the node we do the same
node.children.push(objToJSON(tagArray, tagArray[id].children[i] - 1, node));
}
return node
}
/**
* Draws a tree out of input using d3 library.
* @constructor
* @param {array} selectString - id of element that will contain a tree.
* @param {array} treeData - Final JSON object.
* @param {array} maxDepth - Maximum depth of the tree (for controling of drawn tree height).
* @param {array} maxWidth - Maximum width of the tree (for controling of drawn tree width).
*/
function drawTree(selectString, treeData, maxDepth, maxWidth) {
var start = new Date().getTime();
var margin = {top: 20, right: 120, bottom: 20, left: 120},
width = maxWidth*400 - margin.right - margin.left,
height = maxDepth*50 - margin.top - margin.bottom;
var i = 0,
duration = 750,
root;
var tree = d3.layout.tree()
.size([height, width])
.children(
function(d) {
return d.children;
}
);
var diagonal = d3.svg.diagonal()
.projection(function(d) { return [d.y, d.x]; });
var svg = d3.select("body").append("svg")
.attr("width", width + margin.right + margin.left)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
root = treeData[0];
root.x0 = height / 2;
root.y0 = 0;
function collapse(d) {
if (d.children) {
if (d.children.length) {
d._children = d.children;
d._children.forEach(collapse);
d.children = null;
}
}
}
root.children.forEach(collapse);
update(root);
d3.select(self.frameElement).style("height", "800px");
function update(source) {
// Compute the new tree layout.
var nodes = tree.nodes(root).reverse(),
links = tree.links(nodes);
// Normalize for fixed-depth.
nodes.forEach(function(d) { d.y = d.depth * 300; });
// Update the nodes
var node = svg.selectAll("g.node")
.data(nodes, function(d) { return d.ids || (d.ids = ++i); });
// Enter any new nodes at the parent's previous position.
var nodeEnter = node.enter().append("g")
.attr("class", "node")
.attr("transform", function(d) { return "translate(" + source.y0 + "," + source.x0 + ")"; })
.on("click", click);
nodeEnter.append("circle")
.attr("r", 1e-6)
.style("fill", function(d) {
return d._children ? "lightsteelblue" : "#fff";
}); //#fff
function wordwrap2(text) {
return text.split(" ")
}
nodeEnter.append("text")
.attr("x", function(d) { return d.children || d._children ? -10 : 10; })
.attr("dy", ".35em")
.style("fill-opacity", 1e-6)
.each(function (d) {
var lines = [];
var index = 0;
if (d.value) {
lines[index] = d.value;
index += 1;
}
if (d.name) {
lines[index] = d.name;
index += 1;
}
if (d.extra) {
lines[index] = d.extra;
index += 1;
}
if (d.value && d.type) {
lines[index] = d.type;
index += 1;
if (d.attr) {
for (var k = 0; k < d.attr.length; k++) {
lines[index] = d.attr[k];
index += 1;
}
}
}
if (!d.value && d.type) {
if (d.attr && d.attr.length) {
lines[index] = d.attr[0];
index += 1;
}
lines[index] = d.type;
index += 1;
if (d.attr && d.attr.length > 1) {
for (var k = 1; k < d.attr.length; k++) {
lines[index] = d.attr[k];
index += 1;
}
}
}
if (!index) {
lines[index] = "UnknownNode";
}
for (var i = 0; i < lines.length; i++) {
if (i == 0) { // if that's the first line - make it bold
d3.select(this).append("tspan")
.attr("style", "font-weight: bold")
.attr("dy",23)
.attr("x",function(d) {
return d.children || d._children ? -10 : 10; })
.text(lines[i])
} else { // otherwise small font
d3.select(this).append("tspan")
.attr("style", "font: 8px sans-serif;")
.attr("dy",13)
.attr("x",function(d) {
return d.children || d._children ? -10 : 10; })
.text(lines[i])
}
}
});
// Transition nodes to their new position.
var nodeUpdate = node.transition()
.duration(duration)
.attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; });
nodeUpdate.select("circle")
.attr("r", 4.5)
.style("fill", function(d) {
return d._children ? "lightsteelblue" : "#fff";
}); //#fff
nodeUpdate.select("text")
.style("fill-opacity", 1);
// Transition exiting nodes to the parent's new position.
var nodeExit = node.exit().transition()
.duration(duration)
.attr("transform", function(d) { return "translate(" + source.y + "," + source.x + ")"; })
.remove();
nodeExit.select("circle")
.attr("r", 1e-6);
nodeExit.select("text")
.style("fill-opacity", 1e-6);
// Update the links…
var link = svg.selectAll("path.link")
.data(links, function(d) { return d.target.ids; });
// Enter any new links at the parent's previous position.
link.enter().insert("path", "g")
.attr("class", "link")
.attr("d", function(d) {
var o = {x: source.x0, y: source.y0};
return diagonal({source: o, target: o});
});
// Transition links to their new position.
link.transition()
.duration(duration)
.attr("d", diagonal);
// Transition exiting nodes to the parent's new position.
link.exit().transition()
.duration(duration)
.attr("d", function(d) {
var o = {x: source.x, y: source.y};
return diagonal({source: o, target: o});
})
.remove();
// Stash the old positions for transition.
nodes.forEach(function(d) {
d.x0 = d.x;
d.y0 = d.y;
});
}
// Toggle children on click.
function click(d) {
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
update(d);
}
var elapsed = new Date().getTime() - start;
}