/*!
 * NETEYE Activity Indicator jQuery Plugin
 *
 * Copyright (c) 2010 NETEYE GmbH
 * Licensed under the MIT license
 *
 * Author: Felix Gnass [fgnass at neteye dot de]
 * Version: 1.0.0
 */
 
/**
 * Plugin that renders a customisable activity indicator (spinner) using SVG or VML.
 */
(function($) {

	$.fn.activity = function(opts) {
		this.each(function() {
			var $this = $(this);
			var el = $this.data('activity');
			if (el) {
				clearInterval(el.data('interval'));
				el.remove();
				$this.removeData('activity');
			}
			if (opts !== false) {
				opts = $.extend({color: $this.css('color')}, $.fn.activity.defaults, opts);
				
				el = render($this, opts).css('position', 'absolute').prependTo(opts.outside ? 'body' : $this);
				var h = $this.outerHeight() - el.height();
				var w = $this.outerWidth() - el.width();
				var margin = {
					top: opts.valign == 'top' ? opts.padding : opts.valign == 'bottom' ? h - opts.padding : Math.floor(h / 2),
					left: opts.align == 'left' ? opts.padding : opts.align == 'right' ? w - opts.padding : Math.floor(w / 2)
				};
				var offset = $this.offset();
				if (opts.outside) {
					el.css({top: offset.top + 'px', left: offset.left + 'px'});
				}
				else {
					margin.top -= el.offset().top - offset.top;
					margin.left -= el.offset().left - offset.left;
				}
				el.css({marginTop: margin.top + 'px', marginLeft: margin.left + 'px'});
				animate(el, opts.segments, Math.round(10 / opts.speed) / 10);
				$this.data('activity', el);
			}
		});
		return this;
	};
	
	$.fn.activity.defaults = {
		segments: 12,
		space: 3,
		length: 7,
		width: 4,
		speed: 1.2,
		align: 'center',
		valign: 'center',
		padding: 4
	};
	
	$.fn.activity.getOpacity = function(opts, i) {
		var steps = opts.steps || opts.segments-1;
		var end = opts.opacity !== undefined ? opts.opacity : 1/steps;
		return 1 - Math.min(i, steps) * (1 - end) / steps;
	};
	
	/**
	 * Default rendering strategy. If neither SVG nor VML is available, a div with class-name 'busy' 
	 * is inserted, that can be styled with CSS to display an animated gif as fallback.
	 */
	var render = function() {
		return $('<div>').addClass('busy');
	};
	
	/**
	 * The default animation strategy does nothing as we expect an animated gif as fallback.
	 */
	var animate = function() {
	};
	
	/**
	 * Utility function to create elements in the SVG namespace.
	 */
	function svg(tag, attr) {
		var el = document.createElementNS("http://www.w3.org/2000/svg", tag || 'svg');
		if (attr) {
			$.each(attr, function(k, v) {
				el.setAttributeNS(null, k, v);
			});
		}
		return $(el);
	}
	
	if (document.createElementNS && document.createElementNS( "http://www.w3.org/2000/svg", "svg").createSVGRect) {
	
		// =======================================================================================
		// SVG Rendering
		// =======================================================================================
		
		/**
		 * Rendering strategy that creates a SVG tree.
		 */
		render = function(target, d) {
			var innerRadius = d.width*2 + d.space;
			var r = (innerRadius + d.length + Math.ceil(d.width / 2) + 1);
			
			var el = svg().width(r*2).height(r*2);
			
			var g = svg('g', {
				'stroke-width': d.width, 
				'stroke-linecap': 'round', 
				stroke: d.color
			}).appendTo(svg('g', {transform: 'translate('+ r +','+ r +')'}).appendTo(el));
			
			for (var i = 0; i < d.segments; i++) {
				g.append(svg('line', {
					x1: 0, 
					y1: innerRadius, 
					x2: 0, 
					y2: innerRadius + d.length, 
					transform: 'rotate(' + (360 / d.segments * i) + ', 0, 0)',
					opacity: $.fn.activity.getOpacity(d, i)
				}));
			}
			return $('<div>').append(el).width(2*r).height(2*r);
		};
				
		// Check if Webkit CSS animations are available, as they work much better on the iPad
		// than setTimeout() based animations.
		
		if (document.createElement('div').style.WebkitAnimationName !== undefined) {

			var animations = {};
		
			/**
			 * Animation strategy that uses dynamically created CSS animation rules.
			 */
			animate = function(el, steps, duration) {
				if (!animations[steps]) {
					var name = 'spin' + steps;
					var rule = '@-webkit-keyframes '+ name +' {';
					for (var i=0; i < steps; i++) {
						var p1 = Math.round(100000 / steps * i) / 1000;
						var p2 = Math.round(100000 / steps * (i+1) - 1) / 1000;
						var value = '% { -webkit-transform:rotate(' + Math.round(360 / steps * i) + 'deg); }\n';
						rule += p1 + value + p2 + value; 
					}
					rule += '100% { -webkit-transform:rotate(100deg); }\n}';
					document.styleSheets[0].insertRule(rule);
					animations[steps] = name;
				}
				el.css('-webkit-animation', animations[steps] + ' ' + duration +'s linear infinite');
			};
		}
		else {
		
			/**
			 * Animation strategy that transforms a SVG element using setInterval().
			 */
			animate = function(el, steps, duration) {
				var rotation = 0;
				var g = el.find('g g').get(0);
				el.data('interval', setInterval(function() {
					g.setAttributeNS(null, 'transform', 'rotate(' + (++rotation % steps * (360 / steps)) + ')');
				},  duration * 1000 / steps));
			};
		}
		
	}
	else {
		
		// =======================================================================================
		// VML Rendering
		// =======================================================================================
		
		var s = $('<shape>').css('behavior', 'url(#default#VML)').appendTo('body');
			
		if (s.get(0).adj) {
		
			// VML support detected. Insert CSS rules for group, shape and stroke.
			var sheet = document.createStyleSheet();
			$.each(['group', 'shape', 'stroke'], function() {
				sheet.addRule(this, "behavior:url(#default#VML);");
			});
			
			/**
			 * Rendering strategy that creates a VML tree. 
			 */
			render = function(target, d) {
			
				var innerRadius = d.width*2 + d.space;
				var r = (innerRadius + d.length + Math.ceil(d.width / 2) + 1);
				var s = r*2;
				var o = -Math.ceil(s/2);
				
				var el = $('<group>', {coordsize: s + ' ' + s, coordorigin: o + ' ' + o}).css({top: o, left: o, width: s, height: s});
				for (var i = 0; i < d.segments; i++) {
					el.append($('<shape>', {path: 'm ' + innerRadius + ',0  l ' + (innerRadius + d.length) + ',0'}).css({
						width: s,
						height: s,
						rotation: (360 / d.segments * i) + 'deg'
					}).append($('<stroke>', {color: d.color, weight: d.width + 'px', endcap: 'round', opacity: $.fn.activity.getOpacity(d, i)})));
				}
				return $('<group>', {coordsize: s + ' ' + s}).css({width: s, height: s, overflow: 'hidden'}).append(el);
			};
		
			/**
		     * Animation strategy that modifies the VML rotation property using setInterval().
		     */
			animate = function(el, steps, duration) {
				var rotation = 0;
				var g = el.get(0);
				el.data('interval', setInterval(function() {
					g.style.rotation = ++rotation % steps * (360 / steps);
				},  duration * 1000 / steps));
			};
		}
		$(s).remove();
	}

})(jQuery);
/*!
 * NETEYE Activity Indicator jQuery Plugin
 *
 * Copyright (c) 2010 NETEYE GmbH
 * Licensed under the MIT license
 *
 * Author: Felix Gnass [fgnass at neteye dot de]
 * Version: 1.0.0
 */
 
/**
 * Plugin that renders a customisable activity indicator (spinner) using SVG or VML.
 */
(function($) {

	$.fn.activity = function(opts) {
		this.each(function() {
			var $this = $(this);
			var el = $this.data('activity');
			if (el) {
				clearInterval(el.data('interval'));
				el.remove();
				$this.removeData('activity');
			}
			if (opts !== false) {
				opts = $.extend({color: $this.css('color')}, $.fn.activity.defaults, opts);
				
				el = render($this, opts).css('position', 'absolute').prependTo(opts.outside ? 'body' : $this);
				var margin = {
					top: Math.floor(($this.outerHeight() - el.height()) / 2),
					left: Math.floor(($this.outerWidth() - el.width()) / 2)
				};
				var offset = $this.offset();
				if (opts.outside) {
					el.css({top: offset.top + 'px', left: offset.left + 'px'});
				}
				else {
					margin.top -= el.offset().top - offset.top;
					margin.left -= el.offset().left - offset.left;
				}
				el.css({marginTop: margin.top + 'px', marginLeft: margin.left + 'px'});
				animate(el, opts.segments, Math.round(10 / opts.speed) / 10);
				$this.data('activity', el);
			}
		});
		return this;
	};
	
	$.fn.activity.defaults = {
		segments: 12,
		space: 3,
		length: 7,
		width: 4,
		speed: 1.2
	};
	
	$.fn.activity.getOpacity = function(opts, i) {
		var steps = opts.steps || opts.segments-1;
		var end = opts.opacity !== undefined ? opts.opacity : 1/steps;
		return 1 - Math.min(i, steps) * (1 - end) / steps;
	};
	
	/**
	 * Default rendering strategy. If neither SVG nor VML is available, a div with class-name 'busy' 
	 * is inserted, that can be styled with CSS to display an animated gif as fallback.
	 */
	var render = function() {
		return $('<div>').addClass('busy');
	};
	
	/**
	 * The default animation strategy does nothing as we expect an animated gif as fallback.
	 */
	var animate = function() {
	};
	
	/**
	 * Utility function to create elements in the SVG namespace.
	 */
	function svg(tag, attr) {
		var el = document.createElementNS("http://www.w3.org/2000/svg", tag || 'svg');
		if (attr) {
			$.each(attr, function(k, v) {
				el.setAttributeNS(null, k, v);
			});
		}
		return $(el);
	}
	
	if (document.createElementNS && document.createElementNS( "http://www.w3.org/2000/svg", "svg").createSVGRect) {
	
		// =======================================================================================
		// SVG Rendering
		// =======================================================================================
		
		/**
		 * Rendering strategy that creates a SVG tree.
		 */
		render = function(target, d) {
			var innerRadius = d.width*2 + d.space;
			var r = (innerRadius + d.length + Math.ceil(d.width / 2) + 1);
			
			var el = svg().width(r*2).height(r*2);
			
			var g = svg('g', {
				'stroke-width': d.width, 
				'stroke-linecap': 'round', 
				stroke: d.color
			}).appendTo(svg('g', {transform: 'translate('+ r +','+ r +')'}).appendTo(el));
			
			for (var i = 0; i < d.segments; i++) {
				g.append(svg('line', {
					x1: 0, 
					y1: innerRadius, 
					x2: 0, 
					y2: innerRadius + d.length, 
					transform: 'rotate(' + (360 / d.segments * i) + ', 0, 0)',
					opacity: $.fn.activity.getOpacity(d, i)
				}));
			}
			return $('<div>').append(el).width(2*r).height(2*r);
		};
				
		// Check if Webkit CSS animations are available, as they work much better on the iPad
		// than setTimeout() based animations.
		
		if (document.createElement('div').style.WebkitAnimationName !== undefined) {

			var animations = {};
		
			/**
			 * Animation strategy that uses dynamically created CSS animation rules.
			 */
			animate = function(el, steps, duration) {
				if (!animations[steps]) {
					var name = 'spin' + steps;
					var rule = '@-webkit-keyframes '+ name +' {';
					for (var i=0; i < steps; i++) {
						var p1 = Math.round(100000 / steps * i) / 1000;
						var p2 = Math.round(100000 / steps * (i+1) - 1) / 1000;
						var value = '% { -webkit-transform:rotate(' + Math.round(360 / steps * i) + 'deg); }\n';
						rule += p1 + value + p2 + value; 
					}
					rule += '100% { -webkit-transform:rotate(100deg); }\n}';
					document.styleSheets[0].insertRule(rule);
					animations[steps] = name;
				}
				el.css('-webkit-animation', animations[steps] + ' ' + duration +'s linear infinite');
			};
		}
		else {
		
			/**
			 * Animation strategy that transforms a SVG element using setInterval().
			 */
			animate = function(el, steps, duration) {
				var rotation = 0;
				var g = el.find('g g').get(0);
				el.data('interval', setInterval(function() {
					g.setAttributeNS(null, 'transform', 'rotate(' + (++rotation % steps * (360 / steps)) + ')');
				},  duration * 1000 / steps));
			};
		}
		
	}
	else {
		
		// =======================================================================================
		// VML Rendering
		// =======================================================================================
		
		var s = $('<shape>').css('behavior', 'url(#default#VML)').appendTo('body');
			
		if (s.get(0).adj) {
		
			// VML support detected. Insert CSS rules for group, shape and stroke.
			var sheet = document.createStyleSheet();
			$.each(['group', 'shape', 'stroke'], function() {
				sheet.addRule(this, "behavior:url(#default#VML);");
			});
			
			/**
			 * Rendering strategy that creates a VML tree. 
			 */
			render = function(target, d) {
			
				var innerRadius = d.width*2 + d.space;
				var r = (innerRadius + d.length + Math.ceil(d.width / 2) + 1);
				var s = r*2;
				var o = -Math.ceil(s/2);
				
				var el = $('<group>', {coordsize: s + ' ' + s, coordorigin: o + ' ' + o}).css({top: o, left: o, width: s, height: s});
				for (var i = 0; i < d.segments; i++) {
					el.append($('<shape>', {path: 'm ' + innerRadius + ',0  l ' + (innerRadius + d.length) + ',0'}).css({
						width: s,
						height: s,
						rotation: (360 / d.segments * i) + 'deg'
					}).append($('<stroke>', {color: d.color, weight: d.width + 'px', endcap: 'round', opacity: $.fn.activity.getOpacity(d, i)})));
				}
				return $('<group>', {coordsize: s + ' ' + s}).css({width: s, height: s, overflow: 'hidden'}).append(el);
			};
		
			/**
		     * Animation strategy that modifies the VML rotation property using setInterval().
		     */
			animate = function(el, steps, duration) {
				var rotation = 0;
				var g = el.get(0);
				el.data('interval', setInterval(function() {
					g.style.rotation = ++rotation % steps * (360 / steps);
				},  duration * 1000 / steps));
			};
		}
		$(s).remove();
	}

})(jQuery);
/*!
 * NETEYE Transform & Transition Plugin
 *
 * Copyright (c) 2010 NETEYE GmbH
 * Licensed under the MIT license
 *
 * Author: Felix Gnass [fgnass at neteye dot de]
 * Version: 1.0.0
 */
(function($) {
	
	// ==========================================================================================
	// Private functions
	// ==========================================================================================
	
	var props = (function() {
	
		var prefixes = ['Webkit', 'Moz', 'O'];
		
		var style = document.createElement('div').style;
			  
		function findProp(name) {
			var result = '';
			if (style[name] !== undefined) {
				return name;
			}
			$.each(prefixes, function() {
				var p = this + name.charAt(0).toUpperCase() + name.substring(1);
				if (style[p] !== undefined) {
					result = p;
					return false;
				}
			});
			return result;
		}
		
		var result = {};
		$.each(['transitionDuration', 'transitionProperty', 'transform', 'transformOrigin'], function() {
			result[this] = findProp(this);
		});
		return result;
		
	})();
	
	var supports3d = (function() {
		var s = document.createElement('div').style;
		try {
			s[props.transform] = 'translate3d(0,0,0)';
			return s[props.transform].length > 0;
		}
		catch (ex) {
			return false;
		}
	})();
	
	
	function transform(el, commands) {
		var t = el.data('transform');
		if (!t) {
			t = new Transformation();
			el.data('transform', t);
		}
		if (commands !== undefined) {
			if (commands === false || commands.reset) {
				t.reset();
			}
			else {
				t.exec(commands);
			}
		}
		return t;
	}
	
	/**
	 * Class that keeps track of numeric values and converts them into a string representation
	 * that can be used as value for the -webkit-transform property. TransformFunctions are used
	 * internally by the Transformation class.
	 *
	 * // Example:
	 *
	 * var t = new TransformFunction('translate3d({x}px,{y}px,{z}px)', {x:0, y:0, z:0});
	 * t.x = 23;
	 * console.assert(t.format() == 'translate3d(23px,0px,0px)')
	 */
	function TransformFunction(pattern, defaults) {
		function fillIn(pattern, data) {
			return pattern.replace(/\{(\w+)\}/g, function(s, p1) { return data[p1]; });
		}
		this.reset = function() {
			$.extend(this, defaults);
		};
		this.format = function() {
			return fillIn(pattern, this);
		};
		this.reset();
	}
	
	/**
	 * Class that encapsulates the state of multiple TransformFunctions. The state can be modified
	 * using commands and converted into a string representation that can be used as CSS value.
	 * The class is used internally by the transform plugin.
	 */
	function Transformation() {
		var fn = {
			translate: new TransformFunction('translate({x}px,{y}px)', {x:0, y:0}),
			scale: new TransformFunction('scale({x},{y})', {x:1, y:1}),
			rotate: new TransformFunction('rotate({deg}deg)', {deg:0})
		};
		
		if (supports3d) {
			// Use 3D transforms for better performance
			fn.translate = new TransformFunction('translate3d({x}px,{y}px,0px)', {x:0, y:0});
			fn.scale = new TransformFunction('scale3d({x},{y},1)', {x:1, y:1});
		}	
		
		var commands = {
			rotate: function(deg) {
				fn.rotate.deg = deg;
			},
			rotateBy: function(deg) {
				fn.rotate.deg += deg;
			},
			scale: function(s) {
				if (typeof s == 'number') {
					s = {x: s, y: s};
				}
				fn.scale.x = s.x;
				fn.scale.y = s.y;
			},
			scaleBy: function(s) {
				if (typeof s == 'number') {
					s = {x: s, y: s};
				}
				fn.scale.x *= s.x;
				fn.scale.y *= s.y;
			},
			translate: function(s) {
				var t = fn.translate;
				if (!s) {
					s = {x: 0, y: 0};
				}
				t.x = (s.x !== undefined) ? parseInt(s.x, 10) : t.x;
				t.y = (s.y !== undefined) ? parseInt(s.y, 10) : t.y;
			},
			translateBy: function(s) {
				var t = fn.translate;
				t.x += parseInt(s.x, 10) || 0;
				t.y += parseInt(s.y, 10) || 0;
			}
		};
		this.fn = fn;
		this.exec = function(cmd) {
			for (var n in cmd) {
				if (commands[n]) {
					commands[n](cmd[n]);
				}
			}
		};
		this.reset = function() {
			$.each(fn, function() {
				this.reset();
			});
		};
		this.format = function() {
			var s = '';
			$.each(fn, function(k, v) {
				s += v.format() + ' ';
			});
			return s;
		};
	}
	
	// ==========================================================================================
	// Public API
	// ==========================================================================================
	
	$.fn.transform = function(opts) {
		var result = this;
		if ($.fn.transform.supported) {
			this.each(function() {
				var $this = $(this);
				var t = transform($this, opts);
				if (opts === undefined) {
					result = t.fn;
					return false;
				}
				var origin = opts && opts.origin ? opts.origin : '0 0';
				$this.css(props.transitionDuration, '0s')
					.css(props.transformOrigin, origin)
					.css(props.transform, t.format());
			});
		}
		return result;
	};
	
	$.fn.transform.supported = !!props.transform;
	
	$.fn.transition = function(css, opts) {
	
		opts = $.extend({
			delay: 0,
			duration: 0.4
		}, opts);
		
		var property = '';
		$.each(css, function(k, v) {
			property += k + ',';
		});

		this.each(function() {
			var $this = $(this);
			
			if (!$.fn.transition.supported) {
				$this.css(css);
				if (opts.onFinish) {
					$.proxy(opts.onFinish, $this)();
				}
				return;
			}
			
			var _duration = $this.css(props.transitionDuration);		
			
			function apply() {
				$this.css(props.transitionProperty, property).css(props.transitionDuration, opts.duration + 's');
				
				$this.css(css);
				if (opts.duration > 0) {
					$this.one('webkitTransitionEnd oTransitionEnd transitionend', afterCompletion);
				}
				else {
					setTimeout(afterCompletion, 1);					
				}
			}
			
			function afterCompletion() {
				$this.css(props.transitionDuration, _duration);
					
				if (opts.onFinish) {
					$.proxy(opts.onFinish, $this)();
				}
			}
			
			if (opts.delay > 0) {
				setTimeout(apply, opts.delay);
			}
			else {
				apply();
			}
		});
		return this;
	};
	
	$.fn.transition.supported = !!props.transitionProperty;
	
	$.fn.transformTransition = function(opts) {
		opts = $.extend({
			origin: '0 0',
			css: {}
		}, opts);
		var css = opts.css;
		if ($.fn.transform.supported) {
			css[props.transform] = transform(this, opts).format();
			this.css(props.transformOrigin, opts.origin);
		}
		return this.transition(css, opts);
	};
	
})(jQuery);
/*!
 * NETEYE Touch-Gallery jQuery Plugin
 *
 * Copyright (c) 2010 NETEYE GmbH
 * Licensed under the MIT license
 *
 * Author: Felix Gnass [fgnass at neteye dot de]
 * Version: 1.0.0
 */
(function($) {
	
	var mobileSafari = /Mobile.*Safari/.test(navigator.userAgent);
	
	$.fn.touchGallery = function(opts) {
		opts = $.extend({}, $.fn.touchGallery.defaults, opts);
		var thumbs = this;
		this.live('click', function(ev) {
			ev.preventDefault();
			var clickedThumb = $(this);
			if (!clickedThumb.is('.open')) {
				thumbs.addClass('open');
				openGallery(thumbs, clickedThumb, opts);
			}
		});
		return this;
	};
	
	/**
	 * Default options.
	 */
	$.fn.touchGallery.defaults = {
		getSource: function() {
			return this.href;
		}
	};
	
	// ==========================================================================================
	// Private functions
	// ==========================================================================================
		
	/**
	 * Opens the gallery. A spining activity indicator is displayed until the clicked image has
	 * been loaded. When ready, showGallery() is called.
	 */
	function openGallery(thumbs, clickedThumb, opts) {
		clickedThumb.activity();
		var img = new Image();
		img.onload = function() {
			clickedThumb.activity(false);
			showGallery(thumbs, thumbs.index(clickedThumb), this, opts.getSource);
		};
		img.src = $.proxy(opts.getSource, clickedThumb.get(0))();
	}
	
	/**
	 * Creates DOM elements to actually show the gallery.
	 */
	function showGallery(thumbs, index, clickedImage, getSrcCallback) {
		var viewport = fitToView(preventTouch($('<div id="galleryViewport">').css({
			position: 'fixed',
			top: 0,
			left: 0,
			overflow: 'hidden'
		}).transform(false).appendTo('body')));
		
		var stripe = $('<div id="galleryStripe">').css({
			position: 'absolute',
			height: '100%',
			top: 0,
			left: (-index * getInnerWidth()) + 'px'
		}).width(thumbs.length * getInnerWidth()).transform(false).appendTo(viewport);
		
		setupEventListeners(stripe, getInnerWidth(), index, thumbs.length-1);
		
		$(window).bind('orientationchange.gallery', function() {
			fitToView(viewport);
			stripe.find('img').each(centerImage);
		});
		
		thumbs.each(function(i) {
			var page = $('<div>').addClass('galleryPage').css({
				display: 'block',
				position: 'absolute',
				left: i * getInnerWidth() + 'px',
				overflow: 'hidden',
				height: '100%'
			}).width(getInnerWidth()).data('thumbs', thumbs).data('thumb', $(this)).transform(false).appendTo(stripe);
			
			if (i == index) {
				var $img = $(clickedImage).css({position: 'absolute', display: 'block'}).transform(false);
				makeInvisible(centerImage(index, clickedImage, $img)).appendTo(page);
				zoomIn($(this), $img, function() {
					stripe.addClass('ready');
					loadSurroundingImages(index);
				});
				insertShade(viewport);
			}
			else {
				page.activity({color: '#fff'});
				var img = new Image();
				var src = $.proxy(getSrcCallback, this)();
				page.one('loadImage', function() {
					img.src = src;
				});
				img.onload = function() {
					var $this = $(this).css({position: 'absolute', display: 'block'}).transform(false);
					centerImage(i, this, $this).appendTo(page.activity(false));
					page.trigger('loaded');
				};
			}
		});
	}
	
	function hideGallery(stripe) {
		if (stripe.is('.ready') && !stripe.is('.panning')) {
			$('#galleryShade').remove();
			var page = stripe.find('.galleryPage').eq(stripe.data('galleryIndex'));
			page.data('thumbs').removeClass('open');
			var thumb = page.data('thumb');
			stripe.add(window).add(document).unbind('.gallery');
			zoomOut(page.find('img'), thumb, function() {
				makeVisible(thumb).transform(false);
				$('#galleryViewport').remove();
			});
		}
	}
	
	/**
	 * Inserts a black DIV before the given target element and performs an opacity 
	 * transition form 0 to 1.
	 */
	function insertShade(target, onFinish) {
		var el = $('<div id="galleryShade">').css({
			top: 0, left: 0, background: '#000', opacity: 0
		});
		if (mobileSafari) {
			// Make the shade bigger so that it shadows the surface upon rotation
			var l = Math.max(screen.width, screen.height) * (window.devicePixelRatio || 1) + Math.max(getScrollLeft(), getScrollTop()) + 100;
			el.css({position: 'absolute'}).width(l).height(l);
		}
		else {
			el.css({position: 'fixed', width: '100%', height: '100%'});
		}
		el.insertBefore(target)
		.transform(false)
		.transition({opacity: 1}, {delay: 200, duration: 0.8, onFinish: onFinish});
	}
	
	/**
	 * Scales and centers an element according to the dimensions of the given image.
	 * The first argument is ignored, it's just there so that the function can be used with .each()
	 */
	function centerImage(i, img, el) {
		el = el || $(img);
		if (!img.naturalWidth) {
			//Work-around for Opera which doesn't support naturalWidth/Height. This works because
			//the function is invoked once for each image before it is scaled.
			img.naturalWidth = img.width;
			img.naturalHeight = img.height;
		}
		var s = Math.min(getViewportScale(), Math.min(getInnerHeight()/img.naturalHeight, getInnerWidth()/img.naturalWidth));
		el.css({
			top: Math.round((getInnerHeight() - img.naturalHeight * s) / 2) +  'px',
			left: Math.round((getInnerWidth() - img.naturalWidth * s) / 2) +  'px'
		}).width(Math.round(img.naturalWidth * s));
		return el;
	}
	
	/**
	 * Performs a zoom animation from the small to the large element. The large element is scaled 
	 * down and centered over the small element. Then a transition is performed that 
	 * resets the transformation.
	 */
	function zoomIn(small, large, onFinish) {
		var b = bounds(large);
		var t = bounds(small);
		var s = Math.max(t.width / large.width(), t.height / large.height());
		var ox = mobileSafari ? 0 : getScrollLeft();
		var oy = mobileSafari ? 0 : getScrollTop();
		large.transform({
			translate: {
				x: t.left - b.left - ox - Math.round((b.width * s - t.width) / 2), 
				y: t.top - b.top - oy - Math.round((b.height * s - t.height) / 2)
			}, 
			scale: s
		});
		setTimeout(function() {
			makeVisible(large);
			makeInvisible(small);
			large.transformTransition({reset: true, onFinish: onFinish});
		}, 1);
	}
	
	/**
	 * Performs a zoom animation from the large to the small element. Since the small version
	 * may have a different aspect ratio, the large element is wrapped inside a div and clipped
	 * to match the aspect of the small version. The wrapper div is appended to the body, as 
	 * leaving it in place causes strange z-index/flickering issues.
	 */
	function zoomOut(large, small, onFinish) {
		if (large.length === 0 || !$.fn.transition.supported) {
			if (onFinish) {
				onFinish();
			}
			return;
		}
		var b = bounds(large);
		var t = bounds(small);
		
		var w = Math.min(b.height * t.width / t.height, b.width);
		var h = Math.min(b.width * t.height / t.width, b.height);
		
		var s = Math.max(t.width / w, t.height / h);
		
		var div = $('<div>').css({
			overflow: 'hidden',
			position: 'absolute',
			width: w + 'px',
			height: h + 'px',
			top: getScrollTop() + Math.round((getInnerHeight()-h) / 2) + 'px', 
			left: getScrollLeft() + Math.round((getInnerWidth()-w) / 2) + 'px'
		})
		.appendTo('body').append(large.css({
			top: 1-Math.floor((b.height-h) / 2) + 'px', // -1px offset to match Flickr's square crops
			left: -Math.floor((b.width-w) / 2) + 'px'
		}))
		.transform(false);
		
		b = bounds(div);
		
		div.transformTransition({
			translate: {
				x: t.left - b.left - Math.round((w * s - t.width) / 2), 
				y: t.top - b.top - Math.round((h * s - t.height) / 2)
			}, 
			scale: s,
			onFinish: function() {
				onFinish();
				div.remove();
			}
		});
	}
	
	function getPage(i) {
		return $('#galleryStripe .galleryPage').eq(i);
	}
	
	function getThumb(i) {
		return getPage(i).data('thumb');
	}
	
	function loadSurroundingImages(i) {
		var page = getPage(i);
		function triggerLoad() {
			getPage(i-1).add(getPage(i+1)).trigger('loadImage');
		}
		if (page.find('img').length > 0) {
			triggerLoad();
		}
		else {
			page.one('loaded', triggerLoad);
		}
	}
	
	/**
	 * Registers event listeners to enable flicking through the images.
	 */
	function setupEventListeners(el, pageWidth, currentIndex, max) {
		var scale = getViewportScale();
		var xOffset = parseInt(el.css('left'), 10);
		el.data('galleryIndex', currentIndex);
		
		function flick(dir) {
			var i = el.data('galleryIndex');
			makeVisible(getThumb(i));
			i = Math.max(0, Math.min(i + dir, max));
			el.data('galleryIndex', i);
			makeInvisible(getThumb(i));
			
			loadSurroundingImages(i);
			
			if ($.fn.transform.supported) {
				var x = -i * pageWidth - xOffset;
				if (x != el.transform().translate.x) {
					el.addClass('panning').transformTransition({translate: {x: x}, onFinish: function() { this.removeClass('panning'); }});
				}
			}
			else {
				el.css('left', -i * pageWidth + 'px');
			}
		}
		
		$(document).bind('keydown.gallery', function(event) {
			if (event.keyCode == 37) {
				el.trigger('prev');
			}
			else if (event.keyCode == 39) {
				el.trigger('next');
			}
			if (event.keyCode == 27 || event.keyCode == 32) {
				el.trigger('close');
			}
			return false;
		});
		
		el.bind('touchstart', function() {
			$(this).data('pan', {
				startX: event.targetTouches[0].screenX,
				lastX:event.targetTouches[0].screenX,
				startTime: new Date().getTime(),
				startOffset: $(this).transform().translate.x,
				distance: function() {
					return Math.round(scale * (this.startX - this.lastX));
				},
				delta: function() {
					var x = event.targetTouches[0].screenX;
					this.dir = this.lastX > x ? 1 : -1;
					var delta = Math.round(scale * (this.lastX - x));
					this.lastX = x;
					return delta;
				},
				duration: function() {
					return new Date().getTime() - this.startTime;
				}
			});
			return false;
		})
		.bind('touchmove', function() {
			var pan = $(this).data('pan');
			$(this).transform({translateBy: {x: -pan.delta()}});
			return false;
		})
		.bind('touchend', function() {
			var pan = $(this).data('pan');
			if (pan.distance() === 0 && pan.duration() < 500) {
				$(event.target).trigger('click');
			}
			else {
				flick(pan.dir);
			}
			return false;
		})
		.bind('prev', function() {
			flick(-1);
		})
		.bind('next', function() {
			flick(1);
		})
		.bind('click close', function() {
			hideGallery(el);
		});
	}
	
	/**
	 * Sets position and size of the given jQuery object to match the current viewport dimensions.
	 */
	function fitToView(el) {
		if (mobileSafari) {
			el.css({top: getScrollTop() + 'px', left: getScrollLeft() + 'px'});
		}
		return el.width(getInnerWidth()).height(getInnerHeight());
	}
	
	/**
	 * Returns the reciprocal of the current zoom-factor.
	 * @REVISIT Use screen.width / screen.availWidth instead?
	 */
	function getViewportScale() {
		return getInnerWidth() / document.documentElement.clientWidth;
	}
	
	/**
	 * Returns a window property with fallback to a property on the 
	 * documentElement in Internet Explorer.
	 */
	function getWindowProp(name, ie) {
		if (window[name] !== undefined) {
			return window[name];
		}
		var d = document.documentElement;
		if (d && d[ie]) {
			return d[ie];
		}
		return document.body[ie];
	}
	
	function getScrollTop() {
		return getWindowProp('pageYOffset', 'scrollTop');
	}
	
	function getScrollLeft() {
		return getWindowProp('pageXOffset', 'scrollLeft');
	}
	
	function getInnerWidth() {
		return getWindowProp('innerWidth', 'clientWidth');
	}
	
	function getInnerHeight() {
		return getWindowProp('innerHeight', 'clientHeight');
	}
	
	function makeVisible(el) {
		return el.css('visibility', 'visible');
	}
	
	function makeInvisible(el) {
		return el.css('visibility', 'hidden');
	}
	
	function bounds(el) {
		var e = el.get(0);
		if (e && e.getBoundingClientRect) {
			return e.getBoundingClientRect();
		}
		return $.extend({width: el.width(), height: el.height()}, el.offset());
	}
	
	function preventTouch(el) {
		return el.bind('touchstart', function() { return false; });
	}

})(jQuery);

