API Docs for:
Show:

File: client.js

/**
 * Client module
 *
 * @module client
 */

/**
 * The client file
 *
 * @class Client
 */

$(document).ready(function () {
	
"use strict";

// connect to server
var socket = io.connect("http://localhost");
var REL_SPOUSE = 1; // enum
var REL_CHILD = 2; // enum
var canvas = document.getElementById("timenet_canvas"); 
var WIDTH = Math.floor($(window).width() * 0.9); // canvas width
WIDTH -= WIDTH % 2; // make even
var HEIGHT = 300; // canvas default height
var LINE_THICKNESS = 12; // line thickness
var FEMALE_COLOR = "#bb4444";
var MALE_COLOR = "#4444dd";
var LIGHTNESS_VISIBLE = 0.75; // color for visible but unselected line
var DROPLINE_THICKNESS = 2;
var DROPLINE_COLOR = "#555";
var DROPLINE_FADE_LENGTH = 50; // how much of a dropline should be visible before it fades out
var SPACING = LINE_THICKNESS / 2 + 5; // space between lines
var DEFAULT_CURVE_HEIGHT = 25;
var CURVE_FACTOR = 0.6; // determines how a relationship curve will look
var RELATION_CURVE_LENGTH = WIDTH / 20;
var MIN_RELATION_CURVE_LENGTH = 10; // if a relationship is too short, the curve should have a minimum length
var RELATION_SPACING = 2; // space between curves in a relationship
var DOI_MAX = 10; // doi of selected line
var ALPHA_INVISIBLE;
var ALPHA_INVISIBLE_VISIBLE = 0.1; // opacity of invisible objects (when checked)
var LINE_ANIM_SPEED = 600; // speed of animation
var FONT = "Tahoma, Geneva, sans-serif";

var TIMELINE_COLOR = "#ccc";
var TIMELINE_TEXT_COLOR = "#999";

paper.setup(canvas);

canvas.width = WIDTH;
canvas.height = HEIGHT;
$("#doi_max").text(DOI_MAX);

/**
 * @method arrayDiff
 * @param {Array} a
 * @param {Array} b
 * @return a without the objects of b
 * @for Client
 */
function arrayDiff(a, b) {
	return a.filter(function(i) {return b.indexOf(i) < 0;});
}

/**
 * A local block in the layout (conjugally related persons)
 *
 * @class Block
 * @constructor
 * @param {Array} relationships
 * @param {Array} persons
 * @param {Array} children
 */
function Block(relationships, persons, children) {
	this.relationships = relationships;
	this.persons = persons;
	this.children = children;
}

/**
 * Relationship in a person's life
 *
 * @class EventRelationship
 * @constructor
 * @param {Number} startX
 * @param {Number} endX
 * @param {Number} otherPersonId
 */
function EventRelationship(startX, endX, otherPersonId) {
	this.startX = startX;
	this.endX = endX;
	this.otherPersonId = otherPersonId;
}

/**
 * Start and end of a person's life
 *
 * @class EventExistence
 * @constructor
 * @param {Number} startX
 * @param {Number} endX
 */
function EventExistence(startX, endX) {
	this.startX = startX;
	this.endX = endX;
}

/**
 * Annotation at a point in a person's life
 *
 * @class EventAnnotation
 * @constructor
 * @param {Number} X
 * @param {String} title
 * @param {String} text
 * @param {Date} date
 */
function EventAnnotation(X,title, text, date){
	this.x = X;
	this.title = title;
	this.text = text;
	this.date = date;
}

/**
 * Compare numbers for sorting
 * 
 * @method cmpNumbers
 * @param {Number} a
 * @param {Number} b
 * @return {Number}
 * @for Client
 */
function cmpNumbers(a, b) {
	return a - b;
}

/**
 * Compare events for sorting
 * 
 * @method cmpEvents
 * @param {Event} a
 * @param {Event} b
 * @return {Number}
 * @for Client
 */
function cmpEvents(a, b) {
	return a.startX - b.startX;
}

/**
 * Compare dates for sorting
 * 
 * @method cmpDates
 * @param {Date} a
 * @param {Date} b
 * @return {Number}
 * @for Client
 */
function cmpDates(a, b) {
	return a.getTime() - b.getTime();
}

/**
 * Compare birth dates of persons for sorting
 * 
 * @method cmpBirthDates
 * @param {Person} a
 * @param {Person} b
 * @return {Number}
 * @for Client
 */
function cmpBirthDates(a, b) {
	return cmpDates(a.dateOfBirth, b.dateOfBirth);
}

/**
 * Copy an object
 * 
 * @method copyObject
 * @param {Object} o
 * @return {Object}
 * @for Client
 */
function copyObject(o) {
	return $.extend(true, {}, o);
}

/**
 * Copy an array (copy every object in the array)
 * 
 * @method copyArray
 * @param {Array} a
 * @return {Array}
 * @for Client
 */
function copyArray(a) {
	var r = [];
	a.forEach(function (o) {
		if (o === null || o === undefined) {
			r.push(o);
		} else {
			r.push(copyObject(o));
		}
	});
	return r;
}

/**
 * Sign function
 * 
 * @method sign
 * @param {Number} x
 * @return {Number}
 * @for Client
 */
function sign(x) { 
	return x > 0 ? 1 : x < 0 ? -1 : 0; 
}

/**
 * @method otherPersonId
 * @param {Person} p
 * @param {Relationship} rel
 * @return {Number} id of other person in the relationship
 * @for Client
 */
function otherPersonId(p, rel) {
	if (rel.person1Id === p.id) {
		return rel.person2Id;
	} else {
		return rel.person1Id;
	}
}

/**
 * Called when information about available timenets is received from the server
 *
 * @event timenet_list
 * @param data contains the list of timenet ids
 */
socket.on("timenet_list", function (data) {
	$("#combo_timenets").empty();
	data.timenets.forEach(function (tn) {
		$("<option/>").val(parseInt(tn.id, 10)).text(tn.name).appendTo("#combo_timenets");
	});
});

/**
 * Called when database data is received
 *
 * @event db_data
 * @param data contains the database content from the server
 */
socket.on("db_data", function (data) {
	var persons = [];
	var relationships = [];
	var annotations = [];
	var localBlocks = [];
	var range = 0;
	var y; // current y while drawing
	var i = 0; // person position index
	var firstBirth = null;
	var lastDeath = null;
	var firstBlock;
	var sortedBlocks = [];
	var isDefaultDois = false;
	var DOI_CONJUGAL_RATE = parseInt($("#doi_conjugal_rate").val(), 10);
	var DOI_CHILD_RATE = parseInt($("#doi_child_rate").val(), 10);
	
	/**
	 * Contains data for the current animation
	 *
	 * @class LineAnim
	 * @constructor
	 * @param {Boolean} active
	 * @param {paper.Path} path
	 * @param {Number} from
	 * @param {Number} to
	 * @param {Number} total
	 * @param {Number} sign
	 */
	var lineAnim = {
		active: false,
		path: null,
		from: null,
		to: null,
		total: 0,
		sign: 1
	};
	
	if (data.persons.length === 0) {
		return;
	}
	
	if (isNaN(DOI_CHILD_RATE) || !$.isNumeric(DOI_CHILD_RATE)) {
		DOI_CHILD_RATE = 8;
	}
	
	if (isNaN(DOI_CONJUGAL_RATE) || !$.isNumeric(DOI_CONJUGAL_RATE)) {
		DOI_CONJUGAL_RATE = 1;
	}

	/**
	 * Draws a curve for the current path to endPoint (used in fullRelationCurveTo)
	 * 
	 * @method relationCurveTo
	 * @param {paper.Path} path
	 * @param {paper.Point} endPoint
	 * @for Client
	 */
	function relationCurveTo(path, endPoint) {
		var startPoint = path.lastSegment.point;
		var offset = CURVE_FACTOR * (endPoint.x - startPoint.x);
		path.cubicCurveTo(new paper.Point(startPoint.x + offset, startPoint.y), 
						  new paper.Point(endPoint.x - offset, endPoint.y), 
						  new paper.Point(endPoint.x, endPoint.y));
	}

	/**
	 * Draws a "full relation curve", meaning two curves that are
	 * connected by a straight line
	 * 
	 * @method fullRelationCurveTo
	 * @param {paper.Path} path
	 * @param {paper.Point} endPoint
	 * @param {Number} curveHeight
	 * @for Client
	 */
	function fullRelationCurveTo(path, endPoint, curveHeight) {
		var startPoint = path.lastSegment.point;
		var delta = endPoint.x - startPoint.x;
		var curveLength = RELATION_CURVE_LENGTH;
		if (delta <= RELATION_CURVE_LENGTH * 2) {
			curveLength = delta / 2;
			relationCurveTo(path, new paper.Point(startPoint.x + curveLength, y + curveHeight));
			relationCurveTo(path, new paper.Point(endPoint.x, y));
		} else {
			relationCurveTo(path, new paper.Point(startPoint.x + curveLength, y + curveHeight));
			path.lineTo(startPoint.x + delta - curveLength, y + curveHeight);
			relationCurveTo(path, new paper.Point(endPoint.x, y));
		}
		
		return curveLength;
	}
	
	/**
	 * @method getX
	 * @param {Date} date
	 * @return {Number} x-position on screen for date
	 * @for Client
	 */
	function getX(date) {
		var d;
		if (typeof date.getTime === "function") { // date is a Date
			d = date.getTime();
		} else { // date is a number
			d = date;
		}
		return (d + Math.abs(firstBirth.getTime())) / range * WIDTH;
	}
	
	/**
	 * @method personColor
	 * @param {Person} person
	 * @return {paper.Color} color of person's line, determined by doi and sex
	 * @for Client
	 */
	function personColor(person) {
		var color;
		if (person.sex === "f") {
			color = new paper.Color(FEMALE_COLOR);
		} else if (person.sex === "m") {
			color = new paper.Color(MALE_COLOR);
		}
		
		if (person.doi < DOI_MAX && person.doi > 0) {
			color.lightness = LIGHTNESS_VISIBLE;
		}
		
		return color;
	}

	/**
	 * Insert conjugal relationship and the involved persons into the block; 
	 * then go to their other conjugal relationships, add them to the block,
	 * ... and so on, until the block is completed. 
	 * 
	 * @method fillBlock
	 * @param {Number} relId
	 * @param {Block} block
	 * @for Client
	 */
	function fillBlock(relId, block) {
		if (data.relationships[relId] === undefined || data.relationships[relId] === null) {
			return;
		}
		
		var rel = relationships[relId];
		delete data.relationships[relId];
		
		var person1 = persons[rel.person1Id];
		var person2 = persons[rel.person2Id];
		delete data.persons[rel.person1Id];
		delete data.persons[rel.person2Id];
		
		[person1, person2].forEach(function (p) {
			if (block.persons.indexOf(p) === -1) {
				// add person to block
				block.persons.push(p);
				// add children to block
				p.childRelationships.forEach(function (childRelId) {
					var currentParentId = relationships[childRelId].person2Id;
					var currentChildId = relationships[childRelId].person1Id;
					if (p.id === currentParentId) {
						block.children.push(currentChildId);
					}
				});
				p.block = block;
			}

			p.conjugalRelationships.forEach(function (pRelId) {
				fillBlock(pRelId, block);
			});
		});
	}
	
	/**
	 * Set doi values of all persons to value
	 * 
	 * @method resetDois
	 * @param {Number} value
	 * @for Client
	 */
	function resetDois(value) {
		persons.forEach(function (person) {
			if (person === undefined || person === null) {
				return;
			}
			person.doi = value;
		});
	}
	
	console.log("received data");
	
	// repair the data, especially dates
	data.persons.forEach(function (p) {
		if (p === null) {
			return;
		}
		p.dateOfBirth = new Date(p.dateOfBirth);
		p.dateOfDeath = new Date(p.dateOfDeath);
	});
	data.relationships.forEach(function (r) {
		if (r === null) {
			return;
		}
		r.startDate = new Date(r.startDate);
		r.endDate = new Date(r.endDate);
	});
	data.annotations.forEach(function (a){
		if (a === null){
			return;
		}
		a.date = new Date(a.date);
	});	
	
	// copy data from received data object to client-internal arrays
	persons = copyArray(data.persons);
	relationships = copyArray(data.relationships);
	annotations = copyArray(data.annotations);
	
	resetDois(DOI_MAX);
	isDefaultDois = true;
	
	/**
	 * Generate blocks of relationships
	 * 
	 * @method * generate blocks
	 * @for Client
	 */
	(function () {
		for (var i = 0; i < data.relationships.length; ++i) {
			if (data.relationships[i] === undefined || data.relationships[i] === null) {
				continue;
			} 
			if (data.relationships[i].type === REL_SPOUSE) {
				var block = new Block([data.relationships[i]], [], []);
				fillBlock(i, block);
				
				// remove duplicate block child ids
				block.children = block.children.filter(function (value, index, self) {
					return self.indexOf(value) === index;
				});
				
				block.children.sort(function (a, b) {
					return -cmpBirthDates(persons[a], persons[b]);
				});
				
				console.log("new block size: " + block.persons.length, "children: ", JSON.stringify(block.children));
				localBlocks.push(block);
			}
		}
	})();
	
	/**
	 * Generate 1-person-blocks for persons without relationships, 
	 * preprocess data
	 * 
	 * @method * generate 1-person-blocks
	 * @for Client
	 */
	persons.forEach(function (p) {
		if (p === null || p === undefined) {
			return;
		}
		
		// create blocks for singles
		if (p.conjugalRelationships.length === 0) {
			var block = new Block([], [p], []);
			localBlocks.push(block);
			p.block = block;
		}
		
		// sort child relationships
		p.childRelationships.sort(function (a, b) {
			return cmpDates(relationships[a].startDate, relationships[b].startDate);
		});		
	});
	
	// Find first block to draw: the one with the earliest child birth
	(function () {
		var earliest = {child: -1, block: -1};
		localBlocks.forEach(function (block) {
			if (block.children.length === 0) {
				return;
			}
			
			var earliestInBlock = block.children[block.children.length - 1];
			if (earliest.child === -1 || earliest.parent === -1 || cmpBirthDates(persons[earliestInBlock], persons[earliest.child]) < 0) {
				earliest.child = earliestInBlock;
				earliest.block = block;
			}
		});
		firstBlock = earliest.block;
	})();
	
	/**
	 * Sort the persons of a block by birth date
	 * 
	 * @method sortBlockPersons
	 * @for Client
	 */
	function sortBlockPersons() {
		// sort persons in each block by birth date
		localBlocks.forEach(function (block) {
			block.persons.sort(cmpBirthDates);
		});
	}
	sortBlockPersons();
	
	// sort blocks by earliest birth date
	localBlocks.sort(function (a, b) {
		if (a.persons.length > 0 && b.persons.length > 0) {
			return a.persons[0].dateOfBirth.getTime() - b.persons[0].dateOfBirth.getTime();
		} else if (a.persons.length > 0) {
			return 1;
		} else {
			return -1;
		}
	});

	// get min and max dates of all persons
	persons.forEach(function (person) {
		if (person === null) {
			return;
		}

		if (firstBirth === null || person.dateOfBirth.getTime() < firstBirth.getTime()) {
			firstBirth = person.dateOfBirth;
		}
		if (lastDeath === null || lastDeath.getTime() < person.dateOfDeath.getTime()) {
			lastDeath = person.dateOfDeath;
		}
	});
	range = lastDeath.getTime() - firstBirth.getTime();

	/**
	 * Determine the block order (global layout)
	 * 
	 * @method * sort blocks
	 * @for Client
	 */
	(function () {
		function addChildBlock(childId) {
			if (sortedBlocks.indexOf(persons[childId].block) === -1) {
				sortedBlocks.push(persons[childId].block);
			}
			
			if (persons[childId].childRelationships.length < 0) {
				return;
			}
			
			persons[childId].block.children.forEach(function (subChildId) {
				addChildBlock(subChildId);
			});
		}
		
		function addBlocks(list) {
			list.forEach(function (currentBlock) {
				if (sortedBlocks.indexOf(currentBlock) === -1) {
					sortedBlocks.push(currentBlock);
				}
				currentBlock.children.forEach(function (childId) {
					addChildBlock(childId);
				});
				
				if (sortedBlocks.length === localBlocks.length) {
					return;
				}
				addBlocks(arrayDiff(localBlocks, sortedBlocks));
			});
		}
		addBlocks([firstBlock]);
	})();
	
	/**
	 * As the name says. Must be called every time the layout changes
	 * 
	 * @method drawEverything
	 * @for Client
	 */
	function drawEverything() {
		// clear everything
		paper.project.clear();
		canvas.getContext('2d').clearRect(0, 0, canvas.width, canvas.height);

		y = SPACING*4;

		if ($("#show_invisible_check").prop("checked")) {
			ALPHA_INVISIBLE = ALPHA_INVISIBLE_VISIBLE;
		} else {
			ALPHA_INVISIBLE = 0;
		}

		sortedBlocks.forEach(function (block) {
			
			/**
			*Sorts a persons Life into chronological events and assigns them canvas coordinates.
			*
			* @method * parse a persons Life into Events
			* @param {Person} person  a person of a block
			* @for Client
			*/
			block.persons.forEach(function (person) {			
				if (person === null) {
					return;
				}
				
				var startX = getX(person.dateOfBirth);
				var endX = getX(person.dateOfDeath);
				
				person.lifeline = [new EventExistence(startX, endX)];
				person.hasRelationshipUp = false;
				person.hasRelationshipDown = false;
				person.position = i;
				i++;
				
				person.conjugalRelationships.sort(function (a, b) {
					return relationships[a].startDate.getTime() - relationships[b].startDate.getTime();
				});
				
				person.conjugalRelationships.forEach(function (relId) {
					var rel = relationships[relId];
					var otherPersonId;
					if (person.id === rel.person1Id) {
						otherPersonId = rel.person2Id;
					} else {
						otherPersonId = rel.person1Id;
					}
					var event = new EventRelationship(getX(rel.startDate.getTime()), getX(rel.endDate.getTime()), otherPersonId);
					person.lifeline.push(event);
				});
				person.annotations.forEach(function(annoId){
					var anno = annotations[annoId]
					var event = new EventAnnotation(getX(anno.date.getTime()), anno.title, anno.text, anno.date);
					person.lifeline.push(event);
				});
				
				person.lifeline.sort(cmpEvents);
				//var rel = relationships[person.conjugalRelationships[0]];
			});
			
			// draw lines
				/**
			*Draws Lifelines of a Block
			*
			* @method *draw persons of a block
			* @param {Person} a person in the block Person 
			* @for Client
			*/
			block.persons.forEach(function (person) {
				var path = new paper.Path();
				var vSpacing = SPACING;
				var startX;
				var deathX;
				var curveHeight = DEFAULT_CURVE_HEIGHT;
				var orientation;
				var otherPerson;
				var parentPath = {first: null, second: null};
				var backgroundPath;
				function stopColor(alpha) { 
					var c = new Color(path.strokeColor);
					c.lightness = LIGHTNESS_VISIBLE;
					c.alpha = alpha;
					return c;
				}
				
				function doiAlpha(doi) {
					if (doi <= 0 || doi === DOI_MAX + 1) {
						return ALPHA_INVISIBLE;
					} else  {
						return 1;
					} 
					
				}
				
				path.strokeColor = personColor(person);
				
				if (person.hasRelationshipUp) {
					y += DEFAULT_CURVE_HEIGHT;
					vSpacing += SPACING;
				}
				if (person.hasRelationshipDown) {
					vSpacing += DEFAULT_CURVE_HEIGHT + SPACING;
				}
				vSpacing = DEFAULT_CURVE_HEIGHT * 2 + SPACING * 2;
				/**
				*Draws a person's lifeLine from event to event
				*
				* @method * Draw LifeLines
				* @param {event} event Event in a persons Life
				* @param {Number} currentDoi maximal DOI value
				* @for Client
				*/
				person.lifeline.forEach(function (event) {
					if (event instanceof EventExistence) {
						startX = event.startX;
						path.moveTo(startX, y);
						deathX = event.endX;
						
						// find parent y
						var parents = [];
						person.childRelationships.forEach(function (relId) {
							if (relationships[relId].person1Id === person.id) {
								parents.push(relationships[relId].person2Id);
							}
						});
						if (parents.length >= 2) {
							parentPath.first = persons[parents[0]].path;
							parentPath.second = persons[parents[1]].path;
						}
						/**
						*Writes a person's name and DOI value on their lifeline 
						*
						* @method *write text on LifeLine
						* @for Client
						*/
						(function() {
						//print Text in lifeline
						var textColor = new paper.Color(1);
						textColor.alpha = doiAlpha(person.doi);
						var textLifeLineStyle = {
							  	fillColor: textColor,
								font: FONT,
								fontSize: (LINE_THICKNESS*0.8),
								shadowColor: "black",
								shadowBlur: 1,
								shadowOffset: new paper.Point(1, 0),
								fontWeight: "bold"
							};
						var textLifeLineName = new paper.PointText(new paper.Point(startX+2,y+LINE_THICKNESS *0.3));
						textLifeLineName.content = person.name;
						textLifeLineName.style = textLifeLineStyle;
						
						var textLifeLineDoi = new paper.PointText(new paper.Point(deathX, y+LINE_THICKNESS *0.3));
						textLifeLineDoi.content = person.doi;
						textLifeLineDoi.position.x -=  (textLifeLineDoi.bounds.width +3);
						textLifeLineDoi.style= textLifeLineStyle;
						if (!$("#show_invisible_check").prop("checked")) {
							textLifeLineDoi.visible = false;
						}
						person.textName = textLifeLineName;
						person.textDoi = textLifeLineDoi;
						})();
					} else if (event instanceof EventRelationship) {
						otherPerson = persons[event.otherPersonId];
						
						if (otherPerson.position < person.position) {
							orientation = -1;
							curveHeight = -(y - otherPerson.positionY) + DEFAULT_CURVE_HEIGHT + LINE_THICKNESS + RELATION_SPACING;
						} else {
							orientation = 1;
							curveHeight = orientation * DEFAULT_CURVE_HEIGHT;
						}

						path.lineTo(event.startX, y);
						event.curveLength = fullRelationCurveTo(path, new paper.Point(event.endX, y), curveHeight);
					}
				});
			
				path.lineTo(deathX, y);
					/**
		* Draws a Star on a persons Lifeline, if a special event happened in a persons life.  
		*
		* @method *drawAnnotationSymbol
		* @param {Event} EventAnnotation of a person
		* @for Client
		*/
				person.lifeline.forEach(function (event){
					if (event instanceof EventAnnotation) {
						var intersectionPath = new paper.Path.Line(new paper.Point(event.x, 0), new paper.Point(event.x,y+ DEFAULT_CURVE_HEIGHT));
						var starY = intersectionPath.getIntersections(path)[0].point.y;
						var star = new paper.Path.Star(new paper.Point(event.x,starY), 5,5, 10);
						star.shadowColor = "black";
						star.shadowBlur = 13;
						star.shadowOffset = new paper.Point(1,1);
						star.fillColor = 'yellow';
						star.fillColor.alpha = doiAlpha(person.doi);
						star.data = event;
					}
				});
		/**
		*Draws droplines from parents to children, handels different visibility cases between involved people, and fades lines accordingly.  
		*
		* @method *drawDropLines
		* @for Client
		*/
				// draw drop line
				(function () {
					var dPath;
					var marker;
					var marker2;
					var gradientStops;
					var lineLength;
					var upperParentPath;
					var lowerParentPath;
					var upperParentAlpha;
					var lowerParentAlpha;
					var personAlpha;
					
					function dStopColor(alpha) {
						var c = dPath.strokeColor.clone();
						c.alpha = alpha;
						return c;
					}
					
					// draw drop line
					if (parentPath.first !== null && parentPath.second !== null) {
						dPath = new paper.Path();
						
						dPath.moveTo(startX, y);
						dPath.lineTo(startX, 0);
						parentPath.first.y = parentPath.first.getIntersections(dPath)[0].point.y;
						parentPath.second.y = parentPath.second.getIntersections(dPath)[0].point.y;
						dPath.removeSegments();
						
						if (parentPath.first.y < parentPath.second.y) {
							upperParentPath = parentPath.first;
							lowerParentPath = parentPath.second;
						} else {
							upperParentPath = parentPath.second;
							lowerParentPath = parentPath.first;
						}
						upperParentAlpha = doiAlpha(persons[upperParentPath.data.id].doi);
						lowerParentAlpha = doiAlpha(persons[lowerParentPath.data.id].doi);
						personAlpha = doiAlpha(person.doi);
						
						dPath.strokeColor = DROPLINE_COLOR;
						dPath.strokeWidth = DROPLINE_THICKNESS;
						dPath.dashArray = [2, 2];
						dPath.moveTo(startX, y);
						marker = paper.Shape.Circle(new paper.Point(100, 100), 3);
						marker.fillColor = "#000000";
						
						lineLength = Math.min(DROPLINE_FADE_LENGTH, dPath.firstSegment.point.y - Math.max(upperParentPath.y, lowerParentPath.y) / 2);

						gradientStops = [new paper.GradientStop(dStopColor(personAlpha), 0)];

						// parents are in relationship, only 1 marker
						if (Math.abs(parentPath.first.y - parentPath.second.y) === LINE_THICKNESS + RELATION_SPACING) {
							var parentY = Math.min(parentPath.first.y, parentPath.second.y) + (LINE_THICKNESS + RELATION_SPACING) * 0.5;
							dPath.lineTo(startX, parentY);
							marker.position = new paper.Point(startX, parentY);
							
							// if both parents are invisible
							if (upperParentAlpha === ALPHA_INVISIBLE && 
								lowerParentAlpha === ALPHA_INVISIBLE) {
									
								marker.fillColor.alpha = ALPHA_INVISIBLE;

								gradientStops.push(new paper.GradientStop(dStopColor(ALPHA_INVISIBLE), lineLength / dPath.length));
								gradientStops.push(new paper.GradientStop(dStopColor(ALPHA_INVISIBLE), 1));
							} else {
								marker.fillColor.alpha = dStopColor(1);

								gradientStops.push(new paper.GradientStop(dStopColor(personAlpha), 1 - lineLength / dPath.length));
								gradientStops.push(new paper.GradientStop(dStopColor(1), 1));
							}
						} else { // separate parents
							marker.position = new paper.Point(startX, upperParentPath.y);
							marker2 = marker.clone();
							marker2.position = new paper.Point(startX, lowerParentPath.y);
							dPath.lineTo(startX, marker.position.y);

							// if both parents are invisible
							if (upperParentAlpha === ALPHA_INVISIBLE && 
								lowerParentAlpha === ALPHA_INVISIBLE) {
									
								marker.fillColor.alpha = ALPHA_INVISIBLE;
								marker2.fillColor.alpha = ALPHA_INVISIBLE;
								
								gradientStops.push(new paper.GradientStop(dStopColor(ALPHA_INVISIBLE), lineLength / dPath.length));
								gradientStops.push(new paper.GradientStop(dStopColor(ALPHA_INVISIBLE), 1));
							} else if (upperParentAlpha !== ALPHA_INVISIBLE) { // upper parent visible
								marker.fillColor.alpha = upperParentAlpha;
								if (personAlpha === ALPHA_INVISIBLE && lowerParentAlpha === ALPHA_INVISIBLE) {
									marker2.fillColor.alpha = lowerParentAlpha;
									gradientStops.push(new paper.GradientStop(dStopColor(ALPHA_INVISIBLE), 1 - lineLength / dPath.length));
								} else {
									marker2.fillColor.alpha = upperParentAlpha; // visible, event if line is invisible
									if (personAlpha === ALPHA_INVISIBLE) {
										gradientStops.push(new paper.GradientStop(dStopColor(ALPHA_INVISIBLE), 1 - (Math.abs(upperParentPath.y - lowerParentPath.y) + lineLength) / dPath.length));
									}
									gradientStops.push(new paper.GradientStop(dStopColor(1), 1 - Math.abs(upperParentPath.y - lowerParentPath.y) / dPath.length));
								}
								gradientStops.push(new paper.GradientStop(dStopColor(upperParentAlpha), 1));
							} else if (lowerParentAlpha !== ALPHA_INVISIBLE) { // upper parent invisible, lower parent visible
								marker.fillColor.alpha = upperParentAlpha;
								marker2.fillColor.alpha = lowerParentAlpha;
								if (personAlpha === ALPHA_INVISIBLE) {
									gradientStops.push(new paper.GradientStop(dStopColor(ALPHA_INVISIBLE), 1 - (Math.abs(upperParentPath.y - lowerParentPath.y) + lineLength) / dPath.length));
								}
								gradientStops.push(new paper.GradientStop(dStopColor(1), 1 - Math.abs(upperParentPath.y - lowerParentPath.y) / dPath.length));
								gradientStops.push(new paper.GradientStop(dStopColor(ALPHA_INVISIBLE), 1 - (Math.abs(upperParentPath.y - lowerParentPath.y) - lineLength) / dPath.length));
								gradientStops.push(new paper.GradientStop(dStopColor(ALPHA_INVISIBLE), 1));
							}
						}

						dPath.strokeColor = {
							gradient: {
								stops: gradientStops
							},
							origin: dPath.firstSegment.point,
							destination: dPath.lastSegment.point
						};
					}
				})();
				
				// doi
				path.strokeColor.alpha = doiAlpha(person.doi);
				path.strokeWidth = LINE_THICKNESS;
					/**
				*Fades in invisible Lifelines if the unimportant persons is in a relationship with a more important person.
				*
				* @method *fadeInLifeLines
				* @for Client
				*/
				// faded lines
				if (person.doi <= 0 && person.conjugalRelationships.length > 0) (function () {
					var lineLength = deathX - startX;
					var gradientStops = [new paper.GradientStop(stopColor(ALPHA_INVISIBLE), 0)];

					person.lifeline.forEach(function (event) {
						if (event instanceof EventRelationship && persons[event.otherPersonId].doi > 0  
								&& persons[event.otherPersonId].doi !== DOI_MAX + 1) {
							
							var startRel = (event.startX - startX) / lineLength;
							var endRel = (event.endX - startX) / lineLength;
							var otherAlpha = doiAlpha(persons[event.otherPersonId].doi);
							
							if (event.curveLength < RELATION_CURVE_LENGTH) {
								startRel = (event.startX - MIN_RELATION_CURVE_LENGTH - startX) / lineLength;
								endRel = (event.endX + MIN_RELATION_CURVE_LENGTH - startX) / lineLength;
							}
							
							gradientStops.push(new paper.GradientStop(stopColor(ALPHA_INVISIBLE), startRel));
							gradientStops.push(new paper.GradientStop(stopColor(otherAlpha), (event.startX - startX + event.curveLength / 2) / lineLength));
							gradientStops.push(new paper.GradientStop(stopColor(otherAlpha), (event.endX - startX - event.curveLength / 2) / lineLength));
							gradientStops.push(new paper.GradientStop(stopColor(ALPHA_INVISIBLE), endRel));
						}
					});
					gradientStops.push(new paper.GradientStop(stopColor(ALPHA_INVISIBLE), 1));

					path.strokeColor = {
						gradient: {
							stops: gradientStops
						},
						origin: new paper.Point(startX, y),
						destination: new paper.Point(deathX, y)
					};
				})();
			
				person.vSpacing = vSpacing;
				person.positionY = y;
				y += vSpacing;
				path.data = { id: person.id };
				person.path = path.clone();
				
				path.visible = false;

				// set background path (border)
				//if (person.doi > 0 && person.doi != DOI_MAX + 1) {
					if (!isDefaultDois && person.doi === DOI_MAX) {
						person.path.shadowColor = new paper.Color(0.7, 0.7, 0);
						person.path.shadowBlur = 6;
					} else {
						person.path.shadowColor = new paper.Color(0.2);
						person.path.shadowBlur = 2;
					}
					person.path.shadowOffset = new paper.Point(0, 0);
				//}
							
				//path.fullySelected = true;
			});
		});
			
		/**
		*Draws a timeline on at top and at the bottom of the timeline,, from Birthdate of the oldest person to the deathdate of the youngest.  
		*
		* @method *drawTime
		* @for Client
		*/
		// draw TimeLine
		(function (){
			var year = new Date(Math.ceil(firstBirth.getFullYear()/10) *10, 1,1);
		
			while (lastDeath.getTime() >= year.getTime()) {
				var x = getX(year);
				var vLine = new paper.Path.Line(new paper.Point(x, 0), new paper.Point(x, y));
				vLine.strokeColor = TIMELINE_COLOR;
				vLine.strokeWidth = 1;
				vLine.sendToBack();

				var textTimeLineStyle = {
									fillColor: TIMELINE_TEXT_COLOR,
									font: FONT,
									fontSize: (LINE_THICKNESS)
								};
				
				var textTimeLineTop = new paper.PointText(new paper.Point(x+3, 15));
				textTimeLineTop.content = year.getFullYear();
				textTimeLineTop.style = textTimeLineStyle;
				var textTimeLineBottom = new paper.PointText(new paper.Point(x+3, y-5));
				textTimeLineBottom.style = textTimeLineStyle;	
				textTimeLineBottom.content = textTimeLineTop.content;

				year.setFullYear(year.getFullYear() + 10);
			}
		})();
		console.log("finished drawing");
	}
	drawEverything();
	
	canvas.width = WIDTH;
	canvas.height = y;
	/**
		*Sets the clicked LifeLine in focus, starts the line animation, and starts calculation of Dois. 
		*
		* @method setFocusLine
		* @param {Path} line The lifeline which is new Focus point.
		* @for Client
		*/
	function setFocusLine(line) {
		var person = persons[line.data.id];
		var anim = true;
		
		resetDois(DOI_MAX + 1);
	/**
		*This recursive function calculates the DOI values for every person, starting at the person in focus. 
		*
		* @method setDois
		* @param {Person} p Person in focus
		* @param {Number} currentDoi maximal DOI value
		* @for Client
		*/
		function setDois(p, currentDoi) {
			p.childRelationships.forEach(function (relId) {
				var rel = relationships[relId];
				var otherPerson = persons[otherPersonId(p, rel)];
				if (otherPerson.doi === DOI_MAX + 1) {
					otherPerson.doi = currentDoi - DOI_CHILD_RATE;
				} else {
					otherPerson.doi = Math.max(currentDoi - DOI_CHILD_RATE, otherPerson.doi);
				}
				
				if (currentDoi - DOI_CHILD_RATE > 0) {
					setDois(otherPerson, currentDoi - DOI_CHILD_RATE);
				}
			});
			p.conjugalRelationships.forEach(function (relId) {
				var rel = relationships[relId];
				var otherPerson = persons[otherPersonId(p, rel)];
				if (otherPerson.doi === DOI_MAX + 1) {
					otherPerson.doi = currentDoi - DOI_CONJUGAL_RATE;
				} else {
					otherPerson.doi = Math.max(currentDoi - DOI_CONJUGAL_RATE, otherPerson.doi);
				}
				
				if (currentDoi - DOI_CONJUGAL_RATE > 0) {
					setDois(otherPerson, currentDoi - DOI_CONJUGAL_RATE);
				}
			});
		}
		
		person.doi = DOI_MAX;
		setDois(person, DOI_MAX);
		
		// start animation
		if (!$("#disable_animations_check").prop("checked") && person.block.persons.length > 1 && person.block.persons[0] != person) {
			if (person.path.strokeColor.gradient !== undefined) {
				person.path.strokeColor = person.path.strokeColor.gradient.stops[0].color;
			}
			person.path.strokeColor.alpha = 1;
			person.path.strokeColor = personColor(person);
			person.textName.visible = false;
			person.textDoi.visible = false;
			lineAnim.from = person.path.firstSegment.point.y;
			lineAnim.to = person.block.persons[0].path.firstSegment.point.y;
			lineAnim.path = person.path;
			lineAnim.active = true;
			lineAnim.total = Math.abs(lineAnim.to - lineAnim.from);
			lineAnim.sign = sign(lineAnim.to - lineAnim.from);
			console.log("animation start");
		} else {
			anim = false;
		}
		
		// push line to the start of the block
		var block = person.block;
		var j = block.persons.indexOf(person);
		block.persons.splice(j, 1);
		block.persons.splice(0, 0, person);
		isDefaultDois = false;
		
		// redraw if no animation
		if (!anim) {
			drawEverything();
		}
	}
		/**
	 * Draws annotation textbox. Calculates textbox position and text breaks.
	 *
	 * @method drawAnnotation
	 * @param {Path} AnnotationSymbol includes Position and AnnotationData
	 * @for Client
	 */
	function drawAnnotation(annotationSymbol){
		function breakText(item, maxWidth){
			var shortItem = null;
			if (item.bounds.width > maxWidth){
				shortItem = new paper.PointText();
				var string = "";
				var oldString = "";
				shortItem.content = "";
				var content = item.content.split(" ");
				for(var i = 0; i < content.length; i++){
					if(shortItem.bounds.width < maxWidth){
						oldString = string;
						string += content[i] +" ";
						shortItem.content = string;
						if(shortItem.bounds.width > maxWidth){
							string = oldString+"\n"+ content[i] + " ";
						}
						shortItem.content = string;
					}
					shortItem.content = string;
			}
				shortItem.content = "";
				return string;
			}
			else {
			return item.content;
			}
		}
		var padding = 3;
		var fontSize = 11;
		var annoMaxWidth = 90;
		var x = annotationSymbol.data.x +annotationSymbol.bounds.width/2;
		//console.log(annotationSymbol.position.y)
		var y = annotationSymbol.position.y +LINE_THICKNESS;
		var title = annotationSymbol.data.date.toLocaleDateString()  + ":    " + annotationSymbol.data.title;
		
		var description = annotationSymbol.data.text;
		var annoGroup = new paper.Group();
		var textAnnoStyle = {
			fontSize: fontSize,
			fontFamily: FONT,
			fontColor: "black",
			justification: 'left'
			
		};
		
		var titleItem = new paper.PointText(new paper.Point(x,y));
		titleItem.content = title;
		titleItem.style = textAnnoStyle;
		titleItem.fontWeight = "bold";
		titleItem.content = breakText(titleItem,annoMaxWidth);
		var boundingRectWidth = titleItem.bounds.width;
		var boundingRectHeight = titleItem.bounds.height;
		var textItem = null;
		if (description  !== null){
			textItem = new paper.PointText(x, y + boundingRectHeight + fontSize*0.4);
			textItem.content = description;
			textItem.style = textAnnoStyle;
			textItem.content = breakText(textItem,annoMaxWidth);
			boundingRectWidth = Math.max(boundingRectWidth, textItem.bounds.width);
			boundingRectHeight += textItem.bounds.height;
			
		}
		var boundingRect = new Path.Rectangle(new Point(titleItem.bounds.x - padding-8,titleItem.bounds.y - padding), new Size((boundingRectWidth+8) +2*padding, boundingRectHeight+ 2*padding));
		boundingRect.fillColor = "#ccc";
		
		boundingRect.opacity = 0.9;
		boundingRect.shadowColor = "black";
		boundingRect.shadowBlur = 3;
		boundingRect.shadowOffset = new paper.Point(3,2);
	
		annoGroup.addChild(boundingRect);
		annoGroup.addChild(titleItem);
		if(textItem !== null)
			annoGroup.addChild(textItem);
		
		if(annoGroup.bounds.topRight.x >= WIDTH)
			annoGroup.position.x -= annoGroup.bounds.width;
		
		if(annoGroup.bounds.bottomRight.y >= canvas.height)
			annoGroup.position.y -= annoGroup.bounds.height;
		
		annotationSymbol.moveAbove(annoGroup);
	}
	
	//================================================================================
    // mouse click events
    //================================================================================
    
    // search
	/**
	 * Conducts person search, and sets this person in focus.
	 *
	 * @method selectMatchingLine
	 * @for Client
	 */
    function selectMatchingLine() {
		var query = $("#search_input").val();
		var p;
		
		if (query === null || query === undefined || query === "") {
			return;
		}

		for (var i = 0; i < persons.length; ++i) {
			p = persons[i];
			if (p === null || p === undefined) {
				continue;
			}
			
			if (p.name.toLowerCase().indexOf(query.toLowerCase()) !== -1) {
				setFocusLine(p.path);
				drawEverything();
				break;
			}
		}
	}
	/**
	 * Starts person search on key "enter".
	 *
	 * @event keypress
	 * @for Client
	 */
    $("#search_input").keypress(function(e) {
		if(e.which === 13) { // enter
			selectMatchingLine();
		}
	});
    $("#search_btn").click(selectMatchingLine);
    
    // show-visible checkbox
	/**
	*Displays invisible lines
	*
	* @event show_invisible_check
	* @for Client
	*/
    $("#show_invisible_check").change(function () {
		drawEverything();
	});
	
	// mouse clicks in canvas
	/**
		*  Starts different interactions, depending on which object was clicked.
		*
		 * @event onMouseDown
		 * @for Client
	 */
	paper.install(window);
	var tool = new paper.Tool();
	tool.activate();
	
	tool.onMouseDown = function (event) {
			console.log("click");
			var hitOptions = {
				segments: true,
				stroke: true,
				fill: true,
				tolerance: 2
			};
		var hitResult = paper.project.hitTest(event.point, hitOptions);
		if (hitResult) {
			// we have a hit, do something here
			if( hitResult.item.data instanceof EventAnnotation){
				drawEverything();
				if (hitResult.item.fillColor.alpha === 1) 
						drawAnnotation(hitResult.item);
			}else{
				if($.isEmptyObject(hitResult.item.data)){
					console.log("nothing happening");
				}else{
					setFocusLine(hitResult.item);
					//drawEverything();
				}
			}
		} else {
			resetDois(DOI_MAX);
			sortBlockPersons();
			isDefaultDois = true;
			drawEverything();
		}
		
		
	}
	
	// animations
	/**
	* If animation Event takes place, animate LifeLine
		*
		* @event onFrame Animation
		* @for Client
	*/
	view.onFrame = function (event) {
		var s;
		var dy;
		var remaining;
		var total;
		
		if (lineAnim.active) {
			remaining = Math.abs(lineAnim.path.firstSegment.point.y - lineAnim.to);
			dy = LINE_ANIM_SPEED * event.delta;
			dy = lineAnim.total * 0.02 + dy * (remaining / lineAnim.total);
			lineAnim.path.translate(new Point(0, lineAnim.sign * Math.min(dy, remaining)));
			if (Math.abs(lineAnim.path.firstSegment.point.y - lineAnim.to) <= 0.01) {
				lineAnim.active = false;
				console.log("animation end");
				drawEverything();
			}
		}
	};
	//on window Resize 
	/**
	* If window changes size resize and redraw TimeNets
	*
	 * @event onresize
	 * @for Client
	 */
	$(window).on('resize', function (e) {
			WIDTH = Math.floor($(window).width() * 0.9);
			canvas.width = WIDTH;
			drawEverything();
			canvas.height = y;
		});
});

$("#request").click(function () {
	var id = $("#combo_timenets").val();
	socket.emit("get_data", {id: id});
});
	
});