/*	usemedia.com . joes koppers . 05-07.2010
	thnx for reading this code */


/*	Strik World
*/
function World(link,color)
{
	//defaults
	this.settings = {
		refresh:40,		//redraw speed (ms, 40 = 25fps)
		idle:100,		//idle check (ms)
		watch:150,		//on/offscreen check (ms)
		hitmargin:20,	//click sensitivity for lines (px, min distance)
		cloud:12,		//number of items per cloud
		transition:.055,//transition speed (smooth factor)
				
		item: {
			scale:.75,		//min scale in cloud view (factor, 1=100%)
			speed:0.03,		//move speed (smooth factor)
			zoom:.4,		//zoom speed (smooth factor)
			scroll:.35,		//scroll speed (smooth factor)
			opacity:.65,	//initial opacity
			margin:20,		//min distance to other items
			linefade:1500	//delay before fading lines (ms)
		},
		
		lines: {
			color:color || 'rgb(255,165,0)', //orange
			opacity:.8
		},
		
		info: {
			contact:10,		//preview text length (lines)
			exhibitions:4	//preview text length (lines)
		}
	}
	
	this.state = 'cards'; //cards or info
	this.walls = true;
	this.ego = true;
	this.nav = { prev:false, next:false };
	this.link = link;

	//cards and lines collections
	this.items = [];
	this.lines = [];
	
	//DOM element and initial dimensions
	this.div = $('#world');
	this.resize(true);

	//lines canvas
	var c = $('#strik').get(0);
	if (typeof(G_vmlCanvasManager)=='object') c = G_vmlCanvasManager.initElement(c); //init canvas element for IE
	this.canvas = c;
	this.ctx = c.getContext('2d');
	
	//setup info (contact, exhibitions)
	this.info = new Info(this);
	
	//event handling
	var obj = this;
	$(window).resize(function() {
		obj.resize();
	});
	
	$('body')
		.mousemove(function(e) {
		
			if (!obj.items.length || obj.item || obj.drag || obj.transition) return;

			/*	previous/next cloud line hit-test
			*/
 			var mx = e.clientX;
 			var my = e.clientY;
			
			//previous
			var line = obj.lines[0];
			if (!line.hide)
			{
				var item = obj.items[0];
				if (mx<item.x+(item.w/2))
				{
					var x = item.x + (item.w/2) - mx;
					var y = item.y + (item.h/2) - my;
					line.hittest = {
						type:'prev',
						angle:Math.atan2(x,y),
						distance:Math.sqrt((x*x) + (y*y))
					}
				}
				else line.hittest = false;
			}

			//next
			var line = obj.lines[obj.lines.length-1];
			if (!line.hide)
			{
				var item = obj.items[obj.items.length-1];
				if (mx>item.x+(item.w/2))
				{
					var x = mx - (item.x + (item.w/2));
					var y = my - (item.y + (item.h/2));
					line.hittest = {
						type:'next',
						angle:Math.atan2(x,y),
						distance:Math.sqrt((x*x) + (y*y))
					}
				}
				else line.hittest = false;
			}

			if (obj.idle) obj.drawLines();
		})
	
	this.div
		.mousedown(function(e) { 
			e.preventDefault()
		})
		.click(function() {
			obj.select();
		});

	$('#header')
		.mousedown(function(e) {
			e.preventDefault();
		})
		.find('a')
		.click(function() {
			$(this).blur();
			obj.info.show($(this).text().toLowerCase())
		})
		.end()
		.find('.title')
		.css('cursor','pointer')
		.click(function() {
			//reload most recent cloud
			obj.cloud = 0;
			if (obj.state=='info') obj.setState('cards');
			obj.selectCloud(0);
		});

	
	/*	get data and init
	*/
	$.getJSON('db/cards.php','',function(rsp) { obj.getCards(rsp) });
}

World.prototype.getCards = function(rsp)
{
	/*	cache all cards, 
		deeplink or select first cloud */
		
	if (rsp.isError) return alert(rsp.errorMsg);
	
	this.cards = rsp.items;
	this.clouds = Math.ceil(this.cards.length/this.settings.cloud);
	
	//item deeplink?
	var found
	var id = this.link || Number(window.location.hash.substr(1));
	if (id)
	{
		//find matching cloud and item index within that cloud
		search: 
		for (var cloud=0; cloud<=this.clouds; cloud++)
		{
			var index = 0;
			for (var i=((cloud+1)*this.settings.cloud)-1; i>=cloud*this.settings.cloud; i--) //cards are added in reverse to a cloud..
			{
				if (i>=this.cards.length) continue;
				if (this.cards[i].id==id)
				{
					found = index;
					break search;
				}
				index++;
			}
		}
		this.cloud = found!=undefined? cloud:0;
	}
	else
	{
		//default: most recent items cloud
		this.cloud = 0;
	}
	
	this.selectCloud(this.cloud,found);
}

World.prototype.selectCloud = function(index,select)
{
	/*	show a set of cards, 
		index==0 for most recent items */
		
	var navigate = this.cloud - index; //+1 (older) or -1 (newer)
	this.cloud = index;	

	this.lastcloud = index==0;
	this.firstcloud = index==this.clouds-1;

	//slice cards to items cloud
	var l = this.settings.cloud;
	var items = this.cards.slice(index*l,(index*l)+l)

	this.addItems(items.reverse(),navigate,select);
}

World.prototype.removeItems = function()
{
	for (var i=0; i<this.items.length; i++) this.items[i].dispose();
	this.items = [];
	this.lines = [];
}

World.prototype.addItems = function(cloud,navigate,select)
{
	/*	add selected items to cloud
	*/
	this.removeItems();

	for (var i=0; i<cloud.length; i++)
	{
		//random position
		var x = Math.round(Math.random()*(this.w-150));
		var y = Math.round(Math.random()*(this.h-150));

		//navigate to new cloud?
		if (navigate)
		{
			//center last/first item (keep line jump to minimum)
			if ((navigate<0 && i==cloud.length-1) || (navigate>0 && i==0))
			{
				x = this.w/2; 
				y = this.h/2;
			}
			//start offscreen
			x+= navigate*this.w;
		}
		
		var item = cloud[i];
		var tw,th,mw,mh;

		if (item.text)
		{
			tw = 120; mw = 450;
			th = 80; mh = 306;
		}
		else
		{
			tw = item.image.tw; mw = item.image.w;
			th = item.image.th; mh = item.image.h;
		}
		
		//random scale
		var pct = this.settings.item.scale+(Math.random()*(1-this.settings.item.scale));
		
		if (tw>=th)
		{
			var w = pct * 120;
			var h = (th/tw) * w;
		}
		if (th>tw)
		{
			var h = pct * 120;
			var w = (tw/th) * h;
		}

		//items	
		this.items.push(new Item(this, {
			id:item.id,
			index:i,
			speed:navigate? this.settings.transition:undefined,
			title:item.title,
			year:new Date(item.startdate*1000).getFullYear(),
			x:x, y:y, w:w, h:h, mw:mw, mh:mh,
			text:item.text || false,
			image:item.image? item.image.id:false
		}));
		
		//lines
		var color = this.settings.lines.color;
		if (i>0)
		{
			//previous to current
			this.lines.push({ a:this.items[i-1], b:this.items[i], c:color });
		}
		
		//navigation lines (to next/prev cloud)
		if (i==0) this.lines.push(this.firstcloud? { hide:true }:{ a:{ x:-this.w/2, y:this.h/2,w:0,h:0 }, b:this.items[i], c:color });
		if (i==cloud.length-1) this.lines.push(this.lastcloud? { hide:true }:{ a:this.items[i], b:{ x:this.w+(this.w/2), y:this.h/2,w:0,h:0}, c:color });
	}

	this.resize(); //will init world
	
	if (navigate)
	{
		/*	reset speed when all items are onscreen
			optionally select first/last item of cloud */
			
		var obj = this;
		this.watch(true,function() {
			obj.transition = false;
		
			obj.walls = true;
			for (var i=0; i<obj.items.length; i++) obj.items[i].speed = obj.settings.item.speed;
			
			if (select)
			{
				var index = navigate>0? 0:cloud.length-1;
				obj.select(obj.items[index]);
			}
		});
		
		return;
	}
	
	//auto-select item
	if (select!=undefined) this.select(this.items[select]);
}

World.prototype.resize = function(init)
{
	/*	define window dimensions and center
	*/
	this.w = this.div.width();
	this.h = this.div.height();
	var offset = this.state=='info'? 300:0;
	this.center = {
		x:(this.w/2)+offset,
		y:this.h/2
	}
	
	$('#strik')
		.attr({
			width:this.w,
			height:this.h
		})
		.css({
			width:this.w,
			height:this.h
		})
		
	if (!init) this.init();
}

World.prototype.init = function(stop,speed)
{
	/*	start or stop world
	*/
	if (this.running) window.clearTimeout(this.running);
	if (!stop)
	{
		$.each(this.items, function(i,item) { 
			if (speed) item.speed = speed;
			item.idle = false
			item.setTarget();
		});
		this.t = new Date().getTime();
		this.idle = false;
		this.run();
	}
}

World.prototype.run = function()
{
	/*	world refresh loop, stops when all items are idle
	*/
	if (!this.update()) return;

	var obj = this;
	this.running = window.setTimeout(function() {
		obj.run();
	},this.settings.refresh);
}

World.prototype.update = function()
{
	/*	update all items and lines
	*/

	//idle check?
	if (new Date().getTime()-this.t>this.settings.idle)
	{
		var check = true;
		this.t = new Date().getTime();
	}

	//items
	var idle = 0;
	for (var i=0; i<this.items.length; i++)
	{
		this.items[i].run(check);
		if (this.items[i].idle) idle++;
	}
	
	//lines
	this.drawLines();

	//all items are idle, stop running
	if (idle==this.items.length)
	{
		this.idle = true;
		$('#status').css('border-top-width',0);
		return false;
	}
	
	$('#status').css('border-top-width',1);
	
	return true;
}

World.prototype.drawLines = function(forced)
{
	var obj = this;
	var c = this.ctx;
		c.clearRect(0,0,this.w,this.h);
		c.globalAlpha = this.settings.lines.opacity;

	$.each(this.lines, function(i,line) {
	
		if (line.hide) return true;
		
		c.fillStyle = line.c;
		c.strokeStyle = line.c;
		c.lineWidth = line.hit? 3:1;

		var x1 = line.a.x + (line.a.w/2);
		var y1 = line.a.y + (line.a.h/2);
		var x2 = line.b.x + (line.b.w/2);
		var y2 = line.b.y + (line.b.h/2);

		if (line.hittest)
		{
			var h = line.hittest;
		 	var a = Math.atan2(x2-x1,y2-y1);
			var hit = h.angle && Math.abs(a-h.angle)<.5 && (Math.sin(Math.abs(a-h.angle)) * h.distance)<(obj.settings.hitmargin*2);
 			var is_hit = obj.nav[h.type=='next'? 'prev':'next'];

			//show pointer cursor when hit
			$('body').css('cursor',hit || is_hit? 'pointer':'');
			
			if (is_hit) hit = false; //prevent double hit
			obj.nav[h.type] = hit;
			
			c.lineWidth = hit? 3:1;
		}

		//add arrow to next and previous cloud line
		if (i==0 || i==obj.lines.length-1)
		{
			var r = 60;
			var a = Math.atan2(x2-x1,y2-y1);
			var b = (Math.PI/2) - a; //90 - a
			var g = b - (Math.PI/4); //-45 deg
			
			if (i==0)
			{
				var x = r * Math.cos(b);
				var y = (y2 - (Math.tan(b) * x2)) + (r * Math.sin(b));
			}
			else
			{
				var x = obj.w - (r * Math.cos(b));
				var y = (y1 + (Math.tan((Math.PI/2) - a) * (obj.w-x1))) - (r * Math.sin(b));
				g+= Math.PI; //flip arrow
			}
	
			c.save();
			c.translate(x,y);
			c.rotate(g);
			
			c.beginPath()
			c.moveTo(10,0);
			c.lineTo(0,0);
			c.lineTo(0,10);
			c.stroke();
			
			c.restore();
		}
	
		c.beginPath();
		c.arc(x2,y2,2,0,2*Math.PI,true);
		c.fill();
	
		c.beginPath();
		c.moveTo(x1,y1);
		c.lineTo(x2,y2);
		c.stroke();
	});
	
	if (!forced && this.selected && this.selected.selected) this.selected.card.drawLines();
}

World.prototype.select = function(item,navigate)
{
	/*	(un)select an item
	*/
	this.ego = true;
	if (this.selected) this.selected.select(true);
	if (this.state=='info') this.setState('cards'); //exit info view
	
	if (item)
	{
		if (this.setwalls) window.clearTimeout(this.setwalls);
		this.resetNav();
		this.walls = false;
		this.selected = item.select();
	}
	else
	{
		delete this.selected;
		if (navigate) return;
		if (!this.transition && (this.nav.prev || this.nav.next)) return this.navigate(this.nav.prev? 1:-1);
		
		var obj = this;
		if (!this.transition) this.setwalls = window.setTimeout(function() { obj.walls = true; },800);
	}

	if (this.idle) this.init();
}

World.prototype.navigate = function(navigate,select)
{
	/*	cloud navigation, 
		+1 = older items, -1 = newer items */

	//reset selected and world state
	this.resetNav();
	this.select(false,true);

	//move world center offscreen
	this.center.x = this.center.x + (navigate*1.75*this.w);
	this.walls = false;
	this.transition = true;
	this.init(false,this.settings.transition);

	//wait for items to be offscreen, select new cloud when done
	var obj = this;
	this.watch(false,function() {
		var n = obj.center.x>obj.w/2? -1:1;
		obj.selectCloud(obj.cloud-n,select);
	});
}

World.prototype.resetNav = function()
{
	/*	reset cloud navigation (connecting lines hittests)
	*/
	if (!this.lines.length) return;
	this.lines[0].hittest = false; this.lines[0].hit = false;
	this.lines[this.lines.length-1].hittest = false; this.lines[this.lines.length-1].hit = false;
	this.nav = { prev:false, next:false }
	
	if (this.idle) this.drawLines();
}

World.prototype.watch = function(onscreen,callback)
{
	/*	wait for all items to be on or offscreen
	*/
	if (this.watching) window.clearTimeout(this.watching);
	
	var check = 0;
	$.each(this.items, function(i,item) {
		if (item.onscreen!=onscreen) return false;
		else check++;
	});
	
	if (check==this.items.length) return callback();

	//keep watching
	var obj = this;
	this.watching = window.setTimeout(function() { 
		obj.watch(onscreen,callback) },this.settings.watch
	);
}

World.prototype.setState = function(state)
{
	/*	toggle between info and cards
	*/
	this.state = state;
	
	this.info.div[state=='cards'? 'hide':'show']();
	if (state=='cards') this.info.active = false;
	if (state=='info' && this.selected) this.selected.select(true); //unselect card
	
	//(re)set target
	this.walls = state=='cards'? true:false;
	this.resize();
	this.init();
}


/*	contact and exhibition data
*/
function Info(world)
{
	this.world = world;
	this.active = false;
	this.selected = false;
	this.div = $('#info');
	
	this.contact = { div:$('.info.contact'), cache:new Object() };
	this.exhibitions = { div:$('.info.exhibitions'), cache:new Object() };
}

Info.prototype.show = function(type)
{	
	/*	(un)select info section
	*/
	if (this.active==type) return this.world.setState('cards');
	if (this.world.state=='cards') this.world.setState('info');
	
	this.active = type;
	this.div.find('.info').hide();

	this[type].div.show();

	if (!this[type].div.children().length)
	{
		var obj = this;
		$.getJSON('db/cards.php',{ type:type },function(rsp) { obj.addList(type,rsp) });
	}
}

Info.prototype.addList = function(type,rsp)
{
	/*	(create and) display list of items
	*/
	if (rsp.isError) return alert(rsp.errorMsg);

	var obj = this;
	var select;
	var items = type=='exhibitions'? this.sortExhibitions(rsp.items):rsp.items;	
	
	for (var i=0; i<items.length; i++)
	{
		var item = items[i];
		if (item.id=='header')
		{
			$('<div/>')
				.addClass('list-title')
				.css('margin-top',i>0? 18:0)
				.html(item.title)
				.appendTo(this[type].div);
		}
		else
		{
			if (!select) select = item.id;

			//cache item
			this[type].cache[item.id] = item;
			
			var year = type=='exhibitions'? ', '+new Date(item.startdate*1000).getFullYear():'';
			var title = item.title;
			if (type=='contact') title = title.toUpperCase();

			$('<div/>')
				.addClass('list-item')
				.attr('id','item_'+item.id)
				.html('<a href="javascript://more">'+title+year+'</a><div class="expanded"></div>')
				.appendTo(this[type].div)
				.find('a')
				.css('letter-spacing',type=='contact'? '0.75pt':'')
				.data('id',item.id)
				.click(function() {
					$(this).blur();
					obj.select($(this).data('id'));
				});
		}
	}
	
	this.select(select);
}

Info.prototype.sortExhibitions = function(items)
{
	/*	sort exhibition items by current, upcoming and past
	*/
	var now = new Date().getTime()/1000;
	var p = { upcoming:[], current:[], past:[] };

	for (var i=0; i<items.length; i++)
	{
		var item = items[i];
		var period = 'past';
		if (item.startdate>now) period = 'upcoming';
		else if (item.startdate<now && item.enddate>now) period = 'current';

		p[period].push(item);
	}
	
	//headers
	var h = new Object(); $.each(p, function(id) { h[id] = p[id].length? [{ id:'header', title:id.toUpperCase() }]:[] });
	
	var sorted = [];
	return sorted.concat(
		h.current,
		p.current,
		h.upcoming,
		p.upcoming.reverse(),
		h.past,
		p.past
	);
}

Info.prototype.select = function(id)
{
	/*	expand an info item (line)
	*/
	var type = this.active;
	
	var elm = this[type].div
		.find('.selected')
		.removeClass('selected')
		.find('.expanded')
		.hide();
	var more = elm.find('.navigation a');
	if (more.text()=='LESS') more.click();
		
	var elm = $('#item_'+id)
		.addClass('selected')
		.find('.expanded')
		.show();
		
	//get item contents
	if (elm.html()=='')
	{
		var obj = this;
		$.getJSON('db/item.php',{ id:id, type:type }, function(rsp) { obj.addContent(id,elm,rsp) });
	}
}

Info.prototype.addContent = function(id,elm,rsp)
{
	/*	create info item contents display
	*/
	var obj = this;
	var type = this.active;

	//image and caption for exhibition items
	if (type=='exhibitions')
	{
		//lead image
		if (rsp.images.length)
		{
			$('<img/>')
				.addClass('thumb')
				.attr('src','media/content/'+rsp.images[0].id+'_thumb.jpg')
				.appendTo(elm);
		}
		
		//date, location
		var item = this[type].cache[id];
		var s = new Date(item.startdate*1000);
		var e = new Date(item.enddate*1000);
		var skip = item.startdate==item.enddate;
		var year = e.getFullYear()!=s.getFullYear()? e.getFullYear():'';
		
		$('<div/>')
			.addClass('caption')
			.html(
				(skip? '':'<span class="date">'+s.getDate()+' '+Date.months[s.getMonth()]+' - '+e.getDate()+' '+Date.months[e.getMonth()]+' '+year+'</span><br>')+
				'<span class="location">'+rsp.short.replace('|','<br>')+'</span>'
			)
			.appendTo(elm);
	}
	
	//text contents
	var text = $('<div/>')
		.addClass('text')
		.css({
			'max-height':this.world.settings.info[type]*18
			//'margin-bottom':18
		})
		.html('<div>'+rsp.text+'</div>')
		.appendTo(elm);

	//navigation footer		
	var footer = $('<div/>')
		.addClass('footer')
		.html('<span class="navigation"></span><span class="downloads"></span>')
		.appendTo(elm);
		
		//add expand
		if (text.find('div:first').height()>this.world.settings.info[type]*18)
		{
			$('<a/>')
				.attr('href','javascript://more')
				.html('MORE')
				.appendTo(footer.find('.navigation').after(' &nbsp; '))
				.click(function() {
					
					var p = $(this);
						p.blur().html(p.text()=='MORE'? 'LESS':'MORE')
							
					text.css({
						'max-height':p.text()=='LESS'? '':obj.world.settings.info[type]*18
						//'margin-bottom':p.text()=='LESS'? 0:18
					})
				});
		}
		
		//add downloads
		if (rsp.files)
		{
			var list = [];
			for (var i=0; i<rsp.files.length; i++)
			{
				f = rsp.files[i];
				list.push('<a target="_blank" href="downloads/'+f.file+'">'+f.kind.toUpperCase()+(f.caption? ' - '+f.caption:'')+'</a>');
			}
			footer.find('.downloads').html(list.join(' &nbsp '));
		}
		
		if (footer.find('.navigation, .downloads').text()=='') footer.hide();
}


/*	items
*/
function Item(world,p)
{
	var obj = this;

	this.world = world;
	this.id = p.id;
	this.index = p.index;

	this.title = p.title;
	this.year = p.year;
	this.image = p.image;
	this.text = p.text;
	
	this.cache = false;
	this.card = false;
	
	var s = world.settings.item;
	this.speed = p.speed || s.speed;
	this.zoomspeed = s.zoom;
	this.margin = s.margin;
	this.expanded = false;

	//dimensions
	this.ow = this.w = p.w;
	this.oh = this.h = p.h;
	this.mw = p.mw;
	this.mh = p.mh;


	/*	DOM elements
	*/
	this.div = $('<div/>')
		.addClass('item')
		.css({
			width:this.w,
			height:this.h,
			opacity:this.world.settings.item.opacity
		})
		.appendTo(world.div)
		.click(function(e) {

			if (obj.dragEnd) return (obj.dragEnd = false);
			
			if (!obj.selected)
			{
				/*	select this item
				*/
				world.select(obj);
			}
			else
			{
				/*	select previous or next item (click on lines)
				*/
				var index;
				if (obj.card.nav.prev) index = obj.index-1;
				if (obj.card.nav.next) index = obj.index+1;
				if (index!=undefined)
				{
					if (index<0 || index==world.items.length) world.navigate(index<0? 1:-1,true)
					else world.select(world.items[index]);
				}
			}
			
			e.stopPropagation();
	
		})
		.mouseenter(function() {
			obj.world.item = true;
			obj.world.resetNav();
			if (obj.selected && obj.card && !obj.card.zoomed) obj.card.canvas.show();
			else $(this).css('opacity',1);
		})
		.mouseleave(function() {
			obj.world.item = false;
			if (obj.selected && obj.card) obj.card.fadeLines(true);
			else if (!obj.haspriority) $(this).css('opacity',$(this).data('visited')? 1:obj.world.settings.item.opacity)
		})
		.draggable({
			containment: 'parent',
			distance:5,
			opacity:1,
			start: function(event) {
				obj.dragging = true;
				obj.world.drag = true;
				obj.idle = false;

				if (obj.world.ego && obj.world.idle) obj.world.init();
			},
			drag: function(event,ui) {
				obj.world.drawLines();
				obj.move(ui.position.left,ui.position.top);
			},
			stop: function(event,ui) {

				obj.dragEnd = true;
				obj.world.drag = false;
				obj.dragging = false;
			}
		});

	//add thumb or text
	if (this.image)
	{
		$('<div/>')
			.addClass('thumb')
			.html('<img src="media/content/'+this.image+'_thumb.jpg" style="max-width:'+this.mw+'px; min-width:'+this.w+'px; min-height:'+this.h+'px">')
			.appendTo(this.div);
	}
	else
	{
		//text size/dimensions based on item size
		var s = this.w/120;
		var fs = Math.round(s*13);
		var lh = Math.round(fs*1.2);
		this.textsize = { font:fs, line:lh+'px' };
		$('<div/>')
			.addClass('thumb-text')
			.html(this.text)
			.css({
				'font-size':fs,
				'line-height':lh+'px',
				'margin':s*8,
				'height':4*lh //4 times lineheight (120%)
			})
			.appendTo(this.div);
	}
	
	//css box-shadow workaround for IE
	var elm = this.div.get(0)
	if (typeof(elm.style.filter)=='string')
	{
		elm.style.filter += ' progid:DXImageTransform.Microsoft.shadow(color=#b1b1b1, strength=4, direction=135)';
	}

	this.p = { x:p.x, y:p.y };
	this.move(p.x,p.y);
	
	this.setTarget();
	this.run();
}

Item.prototype.setTarget = function(x,y)
{
	this.tx = x || this.world.center.x - (this.w/2);
	this.ty = y || this.world.center.y - (this.h/2);
}

Item.prototype.checkIdle = function()
{
	this.idle = (this.x==this.p.x && this.y==this.p.y);
	this.p.x = this.x;
	this.p.y = this.y;
	this.onscreen = this.world.walls || (this.x>0 && this.x+this.w<this.world.w && this.y>0 && this.y+this.h<this.world.h);
}

Item.prototype.run = function(check)
{
	/*	item refresh loop, controlled by world.update
	*/
	if (this.dragging || this.zooming) return this.push();
	
	if (check) this.checkIdle();

	//move to target	
	var s = this.speed;
	var dx = this.tx-this.x;
	var dy = this.ty-this.y;
	
	if (this.haspriority && !this.expanded)
	{
		if (Math.abs(dx)<5 && Math.abs(dy)<5) this.expand();
	}

	this.x += Math.round(s*dx);
	this.y += Math.round(s*dy);

	this.move(this.x,this.y);
}

Item.prototype.move = function(x,y,id)
{
	/*	update position
	*/
	if (this.world.walls && (x<0 || x>this.world.w-this.w || y<0 || y>this.world.h-this.h)) return;

	this.x = x;
	this.y = y;

	this.div.css({
		left:this.x,
		top:this.y
	});

	this.push(id);
}

Item.prototype.push = function(id)
{
	/*	move nearby items out of the way,
		an 'each'-loop is used to allow for relaxing the constrains */
	
	var obj = this;
	var pushed = [];
	var x = this.x, y = this.y, r = this.x+this.w, b = this.y+this.h, m = this.margin;
	
	$.each(this.world.items, function(i,item) {
	
		if (item.id==obj.id || item.id==id) return true;
		if (item.haspriority || item.dragging)
		{
			if (item.dragging && obj.world.selected && item!=obj.world.selected) return true;
			item.push();
			return true;
		}
		
		var nx,ny;
		var h = item.y<b && item.y+item.h>y;
		var v = item.x<r+m && item.x+item.w>x-m;
		
		if (h)
		{
			if (item.x<x && item.x+item.w>x-m) nx = x - m - item.w;
			if (item.x>x && item.x<r+m) nx = r + m;
			if (nx!=undefined)
			{
				pushed.push([item,nx,item.y]);
				return true;
			}
		}
		
		if (v)
		{
			if (item.y<y && item.y+item.h>y-m) ny = y - m - item.h;
			if (item.y>y && item.y<b+m) ny = b + m;
			if (ny!=undefined)
			{	
				pushed.push([item,item.x,ny]);
				return true;
			}
		}
	});
	
	//move all pushed items
	for (var i=0; i<pushed.length; i++)
	{
		pushed[i][0].move(pushed[i][1],pushed[i][2],obj.id);
	}
}


/*	select and expand
*/
Item.prototype.select = function(unselect)
{
	/*	(un)select this item
	*/
	if (!unselect)
	{
		//move item to center, expand to card
		this.haspriority = true;
		this.speed = .35;
		this.div.css('opacity',1);
		return this;
	}

	//reset
	if (this.zooming) window.clearTimeout(this.zooming);
	delete this.zooming;

	if (this.card) this.card.dispose();
	delete this.card;

	this.div
		.css({
			width:this.ow,
			height:this.oh,
			opacity:this.selected? 1:this.world.settings.item.opacity
		})
 		.unbind('mousemove')
		.find('.thumb').css('background-color','transparent')
		.find('img').attr('src','media/content/'+this.image+'_thumb.jpg')
		
	if (this.text)
	{
	 	this.div.find('.thumb-text').css({
 			'font-size':this.textsize.font,
 			'line-height':this.textsize.line
 		}).show();
 	}

	this.speed = this.world.settings.item.speed;
	this.w = this.ow;
	this.h = this.oh;
	this.setTarget();
			
	this.haspriority = false;
	this.expanded = false;
	this.selected = false;
	this.idle = false;
}

Item.prototype.expand = function(close)
{
	/*	expand item from thumb to card
	*/
	this.expanded = true;
	
	this.tw = 450; //fixed card width
	this.th = this.mh+160;
	this.tx = this.world.center.x - (this.tw/2);
	this.ty = this.world.center.y - (this.th/2);
	
	this.world.walls = false;
	
	this.div
		.css('opacity',1)
		.find('.thumb-text').css({
			'font-size':12,
			'line-height':'18px'
		}).end()
		.find('.thumb')//.css('background-color',this.mw<this.mh? '#cccccc':'transparent')
		.find('img').attr('src','media/content/'+this.image+'_medium.jpg');

	//start zoom animation
	this.zoom();
}

Item.prototype.zoom = function()
{
	var dw = this.tw-this.w;
	var dh = this.th-this.h;
	this.w = this.w + this.zoomspeed*(this.tw-this.w);
	this.h = this.h + this.zoomspeed*(this.th-this.h);
	
	var update_w = (dw>0 && this.w<this.tw-1) || (dw<0 && this.w>this.tw+1);
	var update_h = (dh>0 && this.h<this.th-1) || (dh<0 && this.h>this.th+1);

	//move to target
	var dx = this.tx-this.x;
	var dy = this.ty-this.y;
	this.x = this.x + this.zoomspeed*(this.tx-this.x);
	this.y = this.y + this.zoomspeed*(this.ty-this.y);
	
	var update_x = (dx>0 && this.x<this.tx-1) || (dx<0 && this.x>this.tx+1);
	var update_y = (dy>0 && this.y<this.ty-1) || (dy<0 && this.y>this.ty+1);

	if (update_w || update_h || update_x || update_y)
	{
		this.move(this.x,this.y);

			this.div.css({
				width:this.w,
				height:this.h
			});

		//loop
		var obj = this;
		this.zooming = window.setTimeout(function() { obj.zoom() },30);
	}
	else
	{
		/*	done, set to exact target position, switch to card gui
		*/
		delete this.zooming;
		this.w = this.tw; this.h = this.th;
		this.x = this.tx; this.y = this.ty;
		this.move(this.x,this.y);
		this.div.css({
			width:this.w,
			height:this.h
		});
		
		//set selected, visited
		this.world.selected = this;
		this.selected = true;
		this.div.data('visited',true);

		//add card gui
		this.card = new Card(this);
	}
}

Item.prototype.dispose = function()
{
	this.div.remove();
}


/*	expanded item
*/
function Card(item)
{
	/*	expanded item gui
	*/
	var obj = this;
	this.item = item;
	this.world = item.world;
	
	//lines canvas
	var c = document.createElement('canvas');
	item.div.append(c);
	if (typeof(G_vmlCanvasManager)=='object') c = G_vmlCanvasManager.initElement(c); //init canvas element for IE
	this.canvas = $(c)
		.addClass('lines')
		.css({
			width:item.w,
			height:item.h
		})
		.attr({
			width:item.w,
			height:item.h
		});
	
	this.fadeLines();

	//keep track of mouse position within card (for lines navigation)
	this.nav = { 
		angle:undefined,
		distance:undefined,
		prev:false,
		next:false
	}
 	item.div.mousemove(function(e) {
 	
 		if (obj.zoomed) return;
		
		obj.canvas.show();
		if (obj.hidelinks) window.clearTimeout(obj.hidelinks);
		
 		/*	store mouse angle and distance to center
 		*/
		var x = e.clientX - obj.item.x - (obj.item.w/2);
		var y = e.clientY - obj.item.y - (obj.item.h/2);
		obj.nav.angle = Math.atan2(x,y);
		obj.nav.distance = Math.sqrt((x*x) + (y*y));
		
		obj.drawLines();
		obj.fadeLines();
	});
	
	//content
	this.div = $('<div/>')
		.addClass('card')
		.html('<div class="card-contents"/>')
		.appendTo(item.div)
		.find('.card-contents')
		//.css('min-height',item.div.find('.card').height());
		
	if (this.item.cache) this.addContent(this.item.cache)
	else 
	{
		//get contents
		$.getJSON('db/item.php',{ id:item.id },function(rsp) { obj.item.cache = rsp; obj.addContent(rsp) });
	}
}

Card.prototype.addContent = function(rsp)
{
	/*	display all card contents
	*/
	if (rsp.isError) return alert(rsp.errorMsg);
	if (!rsp.query.rows) return this.world.select();

	//image
	this.images = [];
	if (this.item.image)
	{
		//add image-zoom container
		var obj = this;
		this.view = $('<div/>')
			.addClass('view')
			.css('height',this.item.mh)
			.appendTo(this.div)
			.click(function(e) {

				if (obj.item.dragEnd) return (obj.item.dragEnd = false);
											
				obj.zoomImage(obj.zoomed,e);
			});
			
		this.images = rsp.images;
		this.image = 0;
	}
	else
	{
		//hide text preview
		this.item.div.find('.thumb-text').hide();
	}
	
	//text
	this.text = $('<div/>')
		.addClass('text')
		.html(
			'<div class="title"><span>'+rsp.title+'</span><span class="caption">, '+this.item.year+'</span></div>'+
			(this.item.image? '<div class="caption"><span>'+this.images[this.image].caption.replace('|','<br/>')+'</span></div>':'')+
			'<div class="thumbs"></div>'+
			'<div class="body"><span>'+rsp.text+'</span></div>'
		)
		.appendTo(this.div)
		.find('span')
		.mousedown(function(e) {
			//do not drag item when over text
 			e.stopImmediatePropagation();
 		});
 		
 	//add mini thumbs
 	if (this.images.length>1)
 	{
		for (var i=0; i<this.images.length; i++)
		{
			$('<div/>')
				.addClass('thumb')
				.css('background-image','url(media/content/'+this.images[i].id+'_mini.jpg)')
				.appendTo(this.div.find('.thumbs'))
				.data('index',i)
				.click(function() {
					obj.selectImage($(this).data('index'));
				});
		}
		this.div.find('.thumbs').append('<div class="clear"></div>');
	}
 		
	//navigation footer
	this.footer = $('<div/>')
		.addClass('footer')
		.html('<span class="navigation"></span><span> &nbsp; </span><span class="downloads"></span>')
		.appendTo(this.item.div);
		
	//add downloads
	if (rsp.files)
	{
		var list = [];
		for (var i=0; i<rsp.files.length; i++)
		{
			f = rsp.files[i];
			list.push('<a target="_blank" href="downloads/'+f.file+'">'+f.kind.toUpperCase()+(f.caption? ' - '+f.caption:'')+'</a>');
		}
		this.footer.find('.downloads').html(list.join(' &nbsp '));
	}
	
	//update scroll navigation
	this.viewh = this.div.parent().height();
	this.scrollh = this.div.height();
	this.scrolld = Math.floor(this.viewh/18)*18; //scroll with factor of lineheight
	this.page = 0;
	this.y = 0;
	this.updateNav();
}

Card.prototype.updateNav = function()
{
	/*	card scroll navigation (more/prev/next)
	*/
	if (this.scrollh<this.viewh) return this.footer.find('span:eq(1)').remove(); //card contents fit
	
	var obj = this;
	var nav = this.footer.find('.navigation').empty();
	var top = -this.div.position().top;
	
	if (top>0)
	{
		//prev
		$('<a/>')
			.attr('href','javascript://prev')
			.html('PREV')
			.appendTo(nav)
			.click(function() {
				obj.navigate('prev');
			});
	}
	if (this.scrollh - top > this.viewh)
	{
		//next
		var next = top==0? 'MORE':'NEXT';
		$('<a/>')
			.attr('href','javascript://'+next.toLowerCase())
			.html(next)
			.css('margin-left',top>0? 10:0)
			.appendTo(nav)
			.click(function() {
				obj.navigate('next');
			});
	}
}

Card.prototype.navigate = function(type)
{
	/*	 scroll card contents
	*/
	if (this.item.image && this.page==0)
	{
		//move thumb to card for visual scroll
		if (this.zoomed) this.zoomImage(true);
		this.view.css('text-align','center').append(this.item.div.find('.thumb:first').hide().find('img:last'));
	}
	
	this.page = type=='prev'? this.page-1:this.page+1;

	if (this.page==0 && this.header) this.header.remove();
	if (this.page==1 && type=='next')
	{
		//add header margin
		this.header = $('<div/>')
			.addClass('header-margin')
			.css('height',this.viewh - this.scrolld)
			.appendTo(this.item.div);
	}

	this.ty = -this.page * this.scrolld;
	this.scroll();
}

Card.prototype.scroll = function()
{
	/*	animated scroll to target
	*/
	var dy = this.ty-this.y;
	this.y = this.y + this.world.settings.item.scroll*(this.ty-this.y);
	
	var update = (dy>0 && this.y<this.ty-1) || (dy<0 && this.y>this.ty+1);
	if (update)
	{
		this.div.css('top',this.y);
		//loop
		var obj = this;
		this.scrolling = window.setTimeout(function() { obj.scroll() },30);
	}
	else
	{
		/*	done, set to exact target position, update navigation
		*/
		delete this.scrolling;
		this.y = this.ty
		this.div.css('top',this.y);

		if (this.item.image)
		{
			//remove thumb from card
			if (this.page==0) this.item.div.find('.thumb:first').show().append(this.view.css('text-align','').find('img'));
		}

		this.updateNav();
	}
}

Card.prototype.selectImage = function(index)
{
	/*	select image by mini thumb, appended to thumb in background due to lines
	*/
	this.image = index;
	if (this.zoomed) this.zoomImage(true);
	
	var image = this.images[this.image];
	var thumb = this.item.div.find('.thumb').find('img.add').remove().end();

	thumb.find('img:first')[index==0? 'show':'hide']();

	if (index>0)
	{
		var w = (this.item.mh<image.h)? (this.item.mh/image.h) * image.w:image.w;
		var m = Math.max(0,(this.item.mh - image.h)/2);
		
		
		thumb.append('<img class="add" src="media/content/'+image.id+'_medium.jpg" style="max-width:'+w+'px; max-height:'+this.item.mh+'px; margin-top:'+m+'px">')
			.find('img:first').hide()
	}
	
	this.div.find('.caption:last').html(image.caption.replace('|','<br/>'))
}

Card.prototype.zoomImage = function(restore,e)
{
	/*	display draggable large image
	*/
	this.zoomed = restore? false:true;
	
	if (restore)
	{
		//remove zoomed image
		return this.view.children().remove();
	}
	
	//hide lines
	this.canvas.hide();
	
	var m = this.images[this.image];
	var w = this.item.w;
	var h = this.item.mh;
	var dx = Math.max(0,m.mw - w);
	var dy = Math.max(0,m.mh - h);
	
	//center to clicked position
	var s = m.mw / this.item.mw;
	var ox = (e.clientX - this.item.x - (this.item.mw/2)) * s;
	var oy = (e.clientY - this.item.y - (this.item.mh/2)) * s;
	
	$('<div/>')
		.addClass('container')
		.css({
			//cursor:this.item.cursor+'zoom-in',
			position:'absolute',
			left:-dx,
			top:-dy,
			width:w + (dx*2),
			height:h + (dy*2)
		})
		.html('<img src="media/content/'+m.id+'_medium.jpg">')
		.appendTo(this.view)
		.find('img')
		.css({
			left:Math.min(dx,Math.max(0,(dx/2)-ox)),
			top:Math.min(dy,Math.max(0,(dy/2)-oy)),
			width:m.mw,
			height:m.mh
		})
		.mousedown(function(e) {
			//prevent dragging of entire item
			e.stopPropagation();
		})
		.draggable({
			containment:'parent',
			cursor:'move'
		})
		.load(function() {
			//update with hires source
			var img = this;
			window.setTimeout(function() { //the tiny delay is for IE stack overflow problem
				img.src = 'media/content/'+m.id+'_large.jpg'
			},10); 
		
			//this.src = 'media/content/'+m.id+'_large.jpg';
		});
}

Card.prototype.drawLines = function()
{
	var p = this.item;
	var c = this.canvas.get(0).getContext('2d');
		c.clearRect(0,0,p.w,p.h);

	//line TO this card
	this.drawLine(c,'prev');
	
	//line FROM this card
	this.drawLine(c,'next');
}

Card.prototype.drawLine = function(c,type)
{
	/*	draw (selected) line on Card
	*/
	var p = this.item;
	var line = this.world.lines[type=='next'? p.index+1:p.index];
	if (line.hide) return;

	var a = type=='next'? 'a':'b';
	var b = type=='next'? 'b':'a';

	var x1 = line[a].x + (line[a].w/2) - p.x;
	var y1 = line[a].y + (line[a].h/2) - p.y;
	var x2 = line[b].x + (line[b].w/2) - p.x;
	var y2 = line[b].y + (line[b].h/2) - p.y;

	//hit-test
	var a = Math.atan2(x2-x1,y2-y1);
	var hit = this.nav.angle && Math.abs(a-this.nav.angle)<1.5 && (Math.sin(Math.abs(a-this.nav.angle)) * this.nav.distance)<this.world.settings.hitmargin;
	var is_hit = this.nav[type=='next'? 'prev':'next'];

	//show pointer cursor when hit
	if (this.text) this.text.add(this.view).css('cursor',hit || is_hit? 'pointer':this.item.dragging? 'move':'');
	
	if (is_hit) hit = false; //prevent double hit
	this.nav[type] = hit;

	c.globalAlpha = .8;
	c.strokeStyle = line.c;
	c.lineWidth = hit? 3:1;
	
	if (type=='next' || p.index==this.world.items.length-1)
	{
		//add center point
		c.beginPath();
		c.arc(x1,y1,2,0,2*Math.PI,true);
		c.fillStyle = line.c;
		c.fill();
	}
	
	c.beginPath();
	c.moveTo(x1,y1);
	c.lineTo(x2,y2);
	c.stroke();

	if (hit) 
	{
		//add arrow to selected line
		c.save();
	
		c.translate(x1,y1);
		c.rotate(d(90) - a);
		
		var x3 = 100; //distance from center
		var y3 = 0;
		var d = Math.sin(d(45)) * 10;
		var x4 = x3 - d;
		var y4 = y3 - d;
	
		c.globalAlpha = 1;
		c.beginPath();
		c.moveTo(x4,y4);
		c.lineTo(x3,y3);
		c.lineTo(x4,y3+d);
		c.stroke();
		
		c.restore();
		
		//highlight prev/next item
 		p.div[type]('.item').css('opacity',1);
	}
	else
	{
		//reset highlighted item
 		p.div[type]('.item').css('opacity',p.div[type]().data('visited')? 1:this.world.settings.item.opacity);
 		this.world.lines[type=='prev'? 0:this.world.lines.length-1].hit = false;
	}

	//highlight or reset prev/next cloud link
	if (type=='prev' && p.index==0)
	{
		 this.world.lines[0].hit = hit;
		 if (this.world.idle) this.world.drawLines(true);
 	}
 	if (type=='next' && p.index==this.world.items.length-1)
 	{
 		 this.world.lines[this.world.lines.length-1].hit = hit;
		 if (this.world.idle) this.world.drawLines(true);
 	}
	
	//deg to rad
	function d(a) { return (Math.PI/180) * a }
}

Card.prototype.fadeLines = function(immediate)
{
	/*	fade lines after delay
	*/
	if (this.hidelinks) window.clearTimeout(this.hidelinks);
	if (immediate) fade(this);
	else
	{
		var obj = this;
 		this.hidelinks = window.setTimeout(function() { fade(obj) },this.world.settings.item.linefade);
 	}

	function fade(obj)
	{
		var d = $.support.opacity? 300:0;
		obj.canvas.fadeOut(d);
		
		obj.item.div
			.next('.item').css('opacity',obj.item.div.next().data('visited')? 1:obj.world.settings.item.opacity).end()
			.prev('.item').css('opacity',obj.item.div.prev().data('visited')? 1:obj.world.settings.item.opacity);
	}
}

Card.prototype.dispose = function()
{
	/*	reset, remove dom elements
	*/
 	if (this.hidelinks) window.clearTimeout(this.hidelinks);
 	if (this.image) this.selectImage(0);
 	if (this.view && this.page!=0) this.item.div.find('.thumb:first').show().append(this.view.css('text-align','').find('img'));
 	
	this.div.parent()
		.add(this.canvas)
		.add(this.footer)
		.add(this.header)
		.remove();
}
