(function() {
var merge = function() {
var merged = {};
for (var i = 0; i < arguments.length; i++) {
var source = arguments[i];
if (typeof source !== 'object') { continue; }
for (var key in source) {
var value = source[key];
if (typeof value !== 'object') {
merged[key] = value;
} else {
if (typeof merged[key] === 'undefined') {
merged[key] = deep_copy(value);
} else {
merged[key] = merge(merged[key], value);
}
}
}
}
return merged;
};
window['merge'] = merge;
var deep_copy = function(source) {
var copied = {};
if (typeof source !== 'object') {
return copied;
}
for (var key in source) {
var value = source[key];
if (typeof value === 'object') {
value = deep_copy(value);
}
copied[key] = value;
}
return copied;
};
var nextNodeID = (function() {
var nodes = 0;
return function() {
return 'ft_node_' + nodes++;
}
})()
window['FamilyTreeJS'] = {
AUTHOR: 'Chandler Prall <chandler.prall@gmail.com>',
VERSION: '.1',
FamilyTree: function() {
var tree_element = null;
var scroll_info = { 'scrolling':false, 'x1':0, 'x2':0, 'y1':0, 'y2':0 };
this.config = {
compressable: true,
node: {
fontcolor: 'black',
background: 'white',
height: 30,
width: 100,
borderwidth: 1,
bordercolor: 'black',
spacingVertical: 40,
spacingHorizontal: 15
},
line: {
offsetY: 0,
width: 2,
color: 'random'
}
};
var people = [];
var Person = function(identity, config, details) {
this.node_id = nextNodeID();
this.identity = identity;
this.details = (typeof details !== 'undefined') ? details : {};
this.parents = [];
this.children = [];
this.leveled = false;
this.level = null;
this.rendered = false;
this.on_grid = false;
this.starting_pos = null;
this.connected = false;
this.node = null;
this.config = config;
this.line_color = null;
this.birth = function(identity, details) {
var config = deep_copy(this.config);
if (typeof details !== 'undefined' && typeof details.config !== 'undefined') {
config = merge(config, details.config);
}
var person = new Person(identity, deep_copy(config), details);
person.parents.push(this);
this.children.push(person);
if (typeof details !== 'undefined' && typeof details.partner !== 'undefined') {
person.parents.push(details.partner);
details.partner.children.push(person);
}
people.push(person);
return person;
};
this.Level = function(level) {
this.level = level;
this.leveled = true;
for (var i = 0; i < this.parents.length; i++) {
if (!this.parents[i].leveled) {
this.parents[i].Level(level - 1);
}
}
for (var i = 0; i < this.children.length; i++) {
if (!this.children[i].leveled) {
this.children[i].Level(level + 1);
}
}
};
this.GetMaxNodeWidth = function() {
var width = 0;
var has_children = false;
for (var i = 0; i < this.children.length; i++) {
if (this.children[i].parents[0] === this) {
has_children = true;
width += this.children[i].GetMaxNodeWidth();
}
}
return width + ((has_children) ? 0 : 1);
};
this.FillGrid = function(grid, starting_pos) {
if (this.on_grid) {
return grid;
}
if (typeof grid[this.level] === 'undefined') {
grid[this.level] = [];
};
this.on_grid = true;
if (this.parents.length > 0) {
if (typeof grid[this.level-1] === 'undefined') {
grid[this.level-1] = [];
}
grid = this.parents[0].FillGrid(grid, starting_pos);
if (this.parents[0].starting_pos > starting_pos) {
starting_pos = this.parents[0].starting_pos;
}
}
while (typeof grid[this.level][starting_pos] !== 'undefined' && grid[this.level][starting_pos] !== null) {
starting_pos++;
}
this.starting_pos = starting_pos;
while (starting_pos > grid[this.level].length) {
grid[this.level].push(null);
}
grid[this.level][starting_pos] = this;
for (var i = 0; i < this.GetMaxNodeWidth() - 1; i++) {
grid[this.level].push(null);
}
var partners = [];
var children = [];
for (var i = 0; i < this.children.length; i++) {
if (this === this.children[i].parents[0]) {
children.push(this.children[i]);
}
for (var j = 0; j < this.children[i].parents.length; j++) {
if (this !== this.children[i].parents[j]) {
var found = false;
for (var k = 0; k < partners.length; k++) {
if (partners[k] === this.children[i].parents[j]) {
found = true;
}
}
if (!found) {
partners.push(this.children[i].parents[j]);
}
}
}
}
for (var i = 0; i < partners.length; i++) {
grid = partners[i].FillGrid(grid, starting_pos + this.GetMaxNodeWidth());
}
var children_aggregate_size = 0;
for (var i = 0; i < children.length; i++) {
grid = children[i].FillGrid(grid, starting_pos + children_aggregate_size);
children_aggregate_size += children[i].GetMaxNodeWidth();
}
return grid;
};
this.IsChildNo = function() {
if (this.parents.length === 0) {
return 0;
}
parent = this.parents[0];
for (var i = 0; i < parent.children.length; i++) {
if (parent.children[i].node_id === this.node_id) {
return i;
}
}
};
this.DrawConnections = function(target) {
this.connected = true;
for (var i = 0; i < this.children.length; i++) {
var child = this.children[i];
if (!child.connected) {
for (var j = 0; j < child.parents.length; j++) {
var parent = child.parents[j];
if (this.line_color === null) {
switch (this.config.line.color) {
case 'random':
this.line_color = 'rgb(' + parseInt(Math.random() * 255) + ',' + parseInt(Math.random() * 255) + ',' + parseInt(Math.random() * 255) + ')';
break;
case 'inherit':
this.line_color = this.parents[0].line_color;
this.config.line.color = this.line_color;
default:
this.line_color = this.config.line.color;
}
}
if (child.parents.length > 0 || this !== parent) {
if (child.parents.length > 1) {
var other_parent = parent;
}
var shape = AutoshapeJS.createShape({
'shape': 'Box',
'color': this.line_color,
'width': this.config.line.width + 'px',
'height': ((this.config.node.spacingVertical / 2) + this.config.line.offsetY) + 'px',
'position': 'absolute',
'left': ((this.config.node.borderwidth * 2) + this.node.element.offsetLeft + (this.config.node.width / 2)) + 'px',
'top': ((this.config.node.borderwidth * 2) + this.node.element.offsetTop + this.config.node.height)+1 + 'px'
});
shape.attachTo(target);
if (typeof other_parent !== 'undefined') {
var shape = AutoshapeJS.createShape({
'shape': 'Box',
'color': this.line_color,
'width': this.config.line.width + 'px',
'height': (((this.level - other_parent.level)*this.config.node.spacingVertical) + ((this.level - other_parent.level) * (this.config.node.spacingVertical / 2))) + ((this.config.node.spacingVertical / 2) + this.config.line.offsetY) + 'px',
'position': 'absolute',
'left': ((this.config.node.borderwidth * 2) + other_parent.node.element.offsetLeft + (this.config.node.width / 2)) + 'px',
'top': ((this.config.node.borderwidth * 2) + other_parent.node.element.offsetTop + this.config.node.height)+1 + 'px'
});
shape.attachTo(target);
var shape = AutoshapeJS.createShape({
'shape': 'Box',
'color': this.line_color,
'width': (this.node.element.offsetLeft < other_parent.node.element.offsetLeft) ?
((this.config.node.borderwidth * 2) + other_parent.node.element.offsetLeft - this.node.element.offsetLeft) + 'px'
: ((this.config.node.borderwidth * 2) + this.node.element.offsetLeft - other_parent.node.element.offsetLeft) + 'px',
'height': this.config.line.width + 'px',
'position': 'absolute',
'left': (this.node.element.offsetLeft < other_parent.node.element.offsetLeft) ?
((this.config.node.borderwidth * 2) + this.node.element.offsetLeft + (this.config.node.width / 2)) + 'px'
: ((this.config.node.borderwidth * 2) + other_parent.node.element.offsetLeft + (this.config.node.width / 2)) + 'px',
'top': ((this.config.node.borderwidth * 2) + other_parent.node.element.offsetTop + this.config.node.height + (this.config.node.spacingVertical / 2) + this.config.line.offsetY) + 'px'
});
shape.attachTo(target);
}
var shape_width = child.node.element.offsetLeft - this.node.element.offsetLeft;
if (shape_width < 0) shape_width *= -1;
var shape_left = (this.node.element.offsetLeft < child.node.element.offsetLeft) ? this.node.element.offsetLeft + (this.config.node.borderwidth*2) + (this.config.node.width / 2) : child.node.element.offsetLeft + (this.config.node.borderwidth*2) + (this.config.node.width / 2) ;
var shape = AutoshapeJS.createShape({
'shape': 'Box',
'color': this.line_color,
'width': shape_width + 'px',
'height': this.config.line.width + 'px',
'position': 'absolute',
'left': shape_left + 'px',
'top': ((this.config.node.borderwidth * 2) + this.node.element.offsetTop + this.config.node.height + (this.config.node.spacingVertical / 2) + this.config.line.offsetY) + 'px'
});
shape.attachTo(target);
var shape = AutoshapeJS.createShape({
'shape': 'Box',
'color': this.line_color,
'width': this.config.line.width + 'px',
'height': (this.config.node.spacingVertical / 2) + 1 + this.config.node.height - (this.config.line.offsetY) + 'px',
'position': 'absolute',
'left': ((this.config.node.borderwidth * 2) + child.node.element.offsetLeft + (this.config.node.width / 2)) + 'px',
'top': ((this.config.node.borderwidth * 2) + child.node.element.offsetTop - (this.config.node.spacingVertical / 2) + this.config.line.offsetY) + 'px'
});
shape.attachTo(target);
child.DrawConnections(target);
}
}
}
}
}
};
this.AddPerson = function(identity, details) {
var config = deep_copy(this.config);
if (typeof details !== 'undefined' && typeof details.config !== 'undefined') {
config = merge(config, details.config);
}
var person = new Person(identity, config, details);
people.push(person);
return person;
};
var MouseCoordinatesFromEvent = function(e) {
var pos = {x:null, y:null};
if (e.pageX || e.pageY) {
pos.x = e.pageX;
pos.y = e.pageY;
} else if (e.clientX || e.clientY) {
pos.x = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft;
pos.y = e.clientY + document.body.scrollTop + document.documentElement.scrollTop;
}
return pos;
};
this.Render = function(element) {
tree_element = element;
tree_element.style.position = 'relative';
tree_element.innerHTML = '';
people[0].Level(0);
min_level = 0;
for (var i = 0; i < people.length; i++) {
if (people[i].level < min_level) {
min_level = people[i].level;
}
}
if (min_level < 0) {
var level_increase = min_level * -1
for (var i = 0; i < people.length; i++) {
people[i].level += level_increase;
}
}
var grid = {};
grid = people[0].FillGrid(grid, 0);
while_loop:
while (true) {
for (var i in grid) {
level = grid[i];
for (var j = 1; j < level.length; j++) {
node = level[j];
if (node !== null && (level[j-1] === null) && node.config.compressable === true) {
if (node.parents.length) {
if (grid[node.level-1] === null || typeof grid[node.level-1] !== 'undefined') {
if (grid[node.level-1][j] !== null && grid[node.level-1][j] === node.parents[0]) {
continue;
}
}
}
var clear_above = true;
var clear_below = true;
if (typeof grid[node.level+1] !== 'undefined') {
if (grid[node.level+1][j-1] !== null && typeof grid[node.level+1][j-1] !== 'undefined' && grid[node.level+1][j-1].parents.length > 0) {
clear_below = false;
for (var k = 0; k < node.children.length; k++) {
if (grid[node.level+1][j-1] === node.children[k]) {
clear_below = true;
}
}
}
}
if (clear_above && clear_below) {
grid[i][j-1] = node;
grid[i][j] = null;
continue while_loop;
}
}
}
}
break;
}
var all_nodes = [];
for (var level in grid) {
var nodes = grid[level];
for (var i = 0; i < nodes.length; i++) {
var node = nodes[i];
if (node !== null) {
this.RenderNode(node, tree_element, level, i);
all_nodes.push(node);
}
}
}
for (var node in all_nodes) {
node = all_nodes[node];
node.DrawConnections(tree_element);
}
tree_element.onselectstart = function() { return false; };
tree_element.unselectable = 'on';
tree_element.style.MozUserSelect = 'none';
tree_element.style.cursor = 'move';
tree_element.onmousedown = function(e) {
scroll_info.scrolling = true;
e = e || window.event;
var mouse_coordinates = MouseCoordinatesFromEvent(e);
scroll_info.x = mouse_coordinates.x;
scroll_info.y = mouse_coordinates.y;
};
tree_element.onmouseup = function(e) {
if (scroll_info.scrolling) {
scroll_info.scrolling = false;
e = e || window.event;
var mouse_coordinates = MouseCoordinatesFromEvent(e);
if (scroll_info.x !== null && scroll_info.y !== null) {
delta = {
x: scroll_info.x - mouse_coordinates.x,
y: scroll_info.y - mouse_coordinates.y
}
tree_element.scrollLeft += delta.x;
tree_element.scrollTop += delta.y;
}
}
};
tree_element.onmousemove = function(e) {
if (scroll_info.scrolling) {
e = e || window.event;
var mouse_coordinates = MouseCoordinatesFromEvent(e);
if (scroll_info.x !== null && scroll_info.y !== null) {
delta = {
x: scroll_info.x - mouse_coordinates.x,
y: scroll_info.y - mouse_coordinates.y
}
tree_element.scrollLeft += delta.x;
tree_element.scrollTop += delta.y;
}
scroll_info.x = mouse_coordinates.x;
scroll_info.y = mouse_coordinates.y;
}
}
tree_element.onmouseout = function(e) {
e = e || window.event;
var element = e.relatedTarget || e.toElement;
if (typeof element !== 'undefined') {
while (element !== null && element !== tree_element) {
element = element.parentNode;
}
if (element !== tree_element) {
scroll_info.scrolling = false;
}
}
}
};
this.RenderNode = function(person, element, level, position) {
var node_left = position * (this.config.node.width + this.config.node.spacingHorizontal);
var shape = AutoshapeJS.createShape({
'shape': 'Box',
'color': person.config.node.background,
'borderwidth': person.config.node.borderwidth + 'px',
'bordercolor': person.config.node.bordercolor,
'width': this.config.node.width + 'px',
'height': this.config.node.height + 'px',
'position': 'absolute',
'left': node_left + 'px',
'top': (level * (this.config.node.height + this.config.node.spacingVertical)) + 'px',
'opacity': (person.identity.length > 0) ? 1 : 0
});
shape.attachTo(element);
shape.element.innerHTML = person.identity;
if (typeof person.details.blurb !== 'undefined') {
shape.element.innerHTML += '<div class="blurb">' + person.details.blurb + '</div>';
}
shape.element.style.color = person.config.node.fontcolor;
shape.element.style.textAlign = 'center';
shape.element.className = 'familytree-node';
person.node = shape;
};
this.center = function() {
var view_center = {
x: tree_element.clientWidth / 2,
y: tree_element.clientHeight / 2
}
if (arguments.length == 0) {
var center = {
x: tree_element.scrollWidth / 2,
y: tree_element.scrollHeight / 2
}
} else {
var node = arguments[0];
var center = {
x: node.node.element.offsetLeft + (node.config.node.width / 2),
y: node.node.element.offsetTop + (node.config.node.height / 2),
}
}
tree_element.scrollLeft = center.x - view_center.x;
tree_element.scrollTop = center.y - view_center.y;
}
}
};
})();