/*
PiAnoS (Picture Annotation System), A fingerprint examiner training tool
Copyright (C) 2011-2012 University of Lausanne, Switzerland
https://ips-labs.unil.ch/pianos

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License
along with this program.  If not, see http://www.gnu.org/licenses/.
*/

/**
 * @constructor
 */
function Pianos4View(params)
{
	// ------------------------------
	// Constructor
	// ------------------------------
	
	params = params || {};
	if (!params.path) { throw 'Pianos4View::Pianos4View() : path required'; };
	if (!params.div) { throw 'Pianos4View::Pianos4View() : div required'; };
	if (!params.image) { throw 'Pianos4View::Pianos4View() : image required'; };
	if (!params.type) params.type = 'mark';
	if (!params.imageEditorScript) params.imageEditorScript = null;
	if (!params.debug) params.debug = false;
	if (typeof(params.splitByUser) == 'undefined') params.splitByUser = false;
	if (!params.editToolHotkey) params.editToolHotkey = 'ctrl';
	if (!params.useOtherFeaturesLayer) params.useOtherFeaturesLayer = false;
	if (typeof(params.inferEditTool) == 'undefined') params.inferEditTool = false;
	if (typeof(params.showModifiedMinutiae) == 'undefined') params.showModifiedMinutiae = false;
	if (!params.updatedMinutiaeTimestamp) params.updatedMinutiaeTimestamp = null;
	if (typeof(params.useH5Canvas) == 'undefined') params.useH5Canvas = false;

	// ------------------------------
	// Public methods
	// ------------------------------
	
	this.init = function()
	{
		annotGroupByContent['minutia'] = 'minutiae';
		annotGroupByContent['ridge'] = 'ridges';
		annotGroupByContent['area'] = 'areas';
		annotGroupByContent['comment'] = 'comments';

		if (params.showModifiedMinutiae && params.updatedMinutiaeTimestamp)
		{
			 params.updatedMinutiaeTimestamp = pgTimestamptzToDate(params.updatedMinutiaeTimestamp);
		}
		
		buildUI();
	}
	
	this.dom = function()
	{
		return params.div;
	}
	
	this.isPrint = function() { return params.type == 'print'; }
	this.isMark = function() { return params.type == 'mark'; }
	this.getType = function() { return params.type; }
	
	this.layout = function()
	{
		var canvas = params.div.find('.pianos4_view_canvas');
		//var svg = params.div.find('svg');
		d3svg.attr('width', canvas.width());
		d3svg.attr('height', canvas.height());
		var box = origin[0]+' '+origin[1]+' '+canvas.width()/zoom+' '+canvas.height()/zoom+'';
		d3svg.attr('viewBox', box);
		
		if (params.useH5Canvas)
		{
			h5Canvas
				/*.css('width', canvas.width())
				.css('height', canvas.height())*/
				.attr('width', canvas.width())
				.attr('height', canvas.height())
				;
			drawH5Canvas();
		}
	}
	
	this.getZoomFitFactor = function()
	{
		var canvas = params.div.find('.pianos4_view_canvas');
		var cratio = canvas.width()/canvas.height();
		
		var iratio = params.image.width / params.image.height;
		
		if (cratio > iratio)
			return canvas.height() / params.image.height;
		else
			return canvas.width() / params.image.width;
	}
	
	this.setZoomFactor = function(factor, pos, resetOrigin, anchorOffset)
	{
		var canvas = params.div.find('.pianos4_view_canvas');

		var newArea = [canvas.width()/factor, canvas.height()/factor];
		
		var offset = canvas.offset();
		
		var posReset = [offset.left+canvas.width()/2.0, offset.top+canvas.height()/2.0];
		
		var center_screen = [offset.left+canvas.width()/2.0, offset.top+canvas.height()/2.0];
		var center_canvas = [params.image.width/2.0, params.image.height/2.0];
		var originReset = [
			center_canvas[0]+(offset.left-center_screen[0])/zoom, 
			center_canvas[1]+(offset.top-center_screen[1])/zoom
		];
		
		if (!pos)
			pos = posReset;
		if (resetOrigin)
			origin = originReset;
		
		// This re-centers the image on the canvas when the view is zoomed out, so that the
		// image stays at the center when smaller than the canvas. It is not perfect for the
		// moment: comparing the viewable area and the image width is not sufficient a criterion.
		// TODO: take the view origin into account (for the moment, there can be jumps if unzooming
		// from a region close to a corner)
		if (!originLocked)
		{
			if (newArea[0] > params.image.width)
			{
				pos[0] = posReset[0];
				origin[0] = originReset[0];
			}
			if (newArea[1] > params.image.height)
			{
				pos[1] = posReset[1];
				origin[1] = originReset[1];
			}
		}
		else if (anchorOffset)
		{
			var px_anchor = self.getAbsPos(originLocked);
			pos = [px_anchor[0]+anchorOffset[0], px_anchor[1]+anchorOffset[1]];
		}
		
		var newOrigin = [
			(pos[0]-offset.left)/zoom+origin[0]-(pos[0]-offset.left)/factor, 
			(pos[1]-offset.top)/zoom+origin[1]-(pos[1]-offset.top)/factor
		];
		
		zoom = factor;
		origin = newOrigin;

		var box = origin[0]+' '+origin[1]+' '+newArea[0]+' '+newArea[1];
		d3svg.attr('viewBox', box);
	}
	
	this.setOrigin = function(pos)
	{
		origin = pos;

		var canvas = params.div.find('.pianos4_view_canvas');
		var box = origin[0]+' '+origin[1]+' '+canvas.width()/zoom+' '+canvas.height()/zoom+'';
		d3svg.attr('viewBox', box);
	}
	
	this.setCenter = function(pos)
	{
		var canvas = params.div.find('.pianos4_view_canvas');
		var offset = canvas.offset();
		var center_screen = [offset.left+canvas.width()/2.0, offset.top+canvas.height()/2.0];
        var o = [
            pos[0]+(offset.left-center_screen[0])/zoom,
            pos[1]+(offset.top-center_screen[1])/zoom
        ]; 
		self.setOrigin(o);
	}
	
	this.setOriginLocked = function(value)
	{
		originLocked = value;
	}
	
	this.getOrigin = function()
	{
		return origin;
	}
	
	this.getZoomFactor = function()
	{
		return zoom;
	}
	
	this.getSVG = function()
	{
		return d3svg;
	}
	
	this.getDOM = function()
	{
		return params.div.find('.pianos4_view_canvas');
	}
	
	this.getCanvasPos = function(abspos)
	{
		var canvas = params.div.find('.pianos4_view_canvas');
		var offset = canvas.offset();
		// There is a glitch here: we calculate the mouse position relative to the canvas object,
		// not the SVG node that is inside. (Note that it makes sense because the SVG object can
		// move freely inside the canvas). Because of the potential border of the canvas (only
		// in comparison mode for the moment, but there could be other uses), the offset() should
		// be adjusted.
		var left = parseInt(canvas.css('border-left-width'));
		var top = parseInt(canvas.css('border-top-width'));
		offset.left += left;
		offset.top += top;
		var ret = [(abspos[0]-offset.left)/zoom+origin[0], (abspos[1]-offset.top)/zoom+origin[1]];
		/*
		// Experimental code, aiming at replacing the stupid equation above and make it more
		// generic. Does not work yet. Tightly coupled with the viewBox of the SVG. Actually, this
		// is the beginning of the refactoring of the zoom&pan, so that the canvas can be freely
		// rotated (can only be panned and zoom for the moment, but adding the rotate invalidates many
		// previous assumptions that are used everywhere in the zoom&pan APIs).
		//log('getCanvasPos of '+abspos);
		var pt = d3svg.node().createSVGPoint();
		pt.x = abspos[0];
		pt.y = abspos[1];
		var globalPoint = pt.matrixTransform(d3svg.node().getScreenCTM().inverse());
		var root = d3svg.select('.pianos4_helpers').node();
		var globalToLocal = root.getTransformToElement(d3svg.node()).inverse();
		var localPoint = globalPoint.matrixTransform(globalToLocal);
		log(ret);
		log(globalPoint);
		log(localPoint);
		return [localPoint.x,localPoint.y];
		*/
		return ret;
	}
	
	this.getAbsPos = function(canvaspos)
	{
		var canvas = params.div.find('.pianos4_view_canvas');
		var offset = canvas.offset();
		// Fix the canvas offset (see comment above)
		var left = parseInt(canvas.css('border-left-width'));
		var top = parseInt(canvas.css('border-top-width'));
		offset.left += left;
		offset.top += top;
		var ret = [((canvaspos[0]-origin[0])*zoom)+offset.left, ((canvaspos[1]-origin[1])*zoom)+offset.top];
		/*
		//log('getAbsPos of '+canvaspos);
		var pt = d3svg.node().createSVGPoint();
		pt.x = canvaspos[0];
		pt.y = canvaspos[1];
		var localPoint = pt.matrixTransform(d3svg.select('.pianos4_root').node().getScreenCTM());
		//log(ret);
		//log(localPoint);
		return [localPoint.x, localPoint.y];
		*/
		return ret;
	}
	
	this.addMinutia = function(minutia)
	{
		assert(minutia && !(minutia.id in annotations.minutiae) && !(minutia.id in allAnnotations));
		annotations.minutiae[minutia.id] = minutia;
		allAnnotations[minutia.id] = ['minutia', minutia];
		
		onAnnotationAdded(minutia);
		
		drawMinutia(minutia);			
	}
	
	this.removeMinutia = function(minutia)
	{
		assert(minutia && minutia.id in annotations.minutiae && minutia.id in allAnnotations);
		delete annotations.minutiae[minutia.id];
		delete allAnnotations[minutia.id];
		
		clearMinutia(minutia);
		
		onAnnotationRemoved(minutia);
	}

	this.updateMinutiaPairing = function(minutia)
	{
		assert(minutia && minutia.id in annotations.minutiae);

		var pairingNode = layers.pairing.select('#minutia_'+minutia.id+'_pairing');

		// var msg = '['+params.type+'] Updating pairing of minutia '+minutia.id+'.';
		// if (!pairingNode.empty())
			// msg += ' Was previously paired.';
		// if ('pairing' in minutia)
			// msg += ' Is now paired as '+minutia.pairing+'.';
		// log(msg);
			
		if (!pairingNode.empty())
			pairingNode.remove();
			
		if ('pairing' in minutia)
		{
			var parent = null;
			if (params.splitByUser)
			{
				parent = layers.pairing.select('#pairing_user_'+minutia.userid);
				if (parent.empty())
					parent = layers.pairing.append('svg:g').attr('id', 'pairing_user_'+minutia.userid);
			}
			else
			{
				parent = layers.pairing;
			}
			assert(parent);
			parent.append('svg:text')
				.attr('id', 'minutia_'+minutia.id+'_pairing')
				.classed('pianos4_editable', true)
				.classed('pianos4_pair', true)
				.attr('x', minutia.pos[0])
				.attr('y', minutia.pos[1])
				.attr('text-anchor', 'middle')
				.attr('dominant-baseline', 'central')
				.attr('font-size', '2em')
				.attr('stroke', '#ffffff')
				.attr('stroke-width', 1)
				//.attr('stroke-opacity', 0.5)
				.attr('fill', '#0000aa')
				//.attr('fill-opacity', 0.7)
				.attr('font-family', 'Courier New')
				.attr('font-weight', 'bold')
				.text(minutia.pairing)
			;
		}

		var m = layers.minutiae.select('#minutia_'+minutia.id);
		assert(m.length == 1);
		var opacity = pianos.getActiveTool() 
			&& pianos.getActiveTool().getName() == 'pairing' 
			&& 'pairing' in minutia ? 0.3 : 1.0; 
		if (opacity != 1.0)
		{
			if (m.attr('stroke-opacity') != opacity)
				m.attr('stroke-opacity', opacity);
		}
		else
		{
			if (m.attr('stroke-opacity'))
				m.node().removeAttribute('stroke-opacity');
		}
	}
	
	this.addRidge = function(ridge)
	{
		assert(ridge && !(ridge.id in annotations.ridges) && !(ridge.id in allAnnotations));
		annotations.ridges[ridge.id] = ridge;
		allAnnotations[ridge.id] = ['ridge', ridge];
		
		onAnnotationAdded(ridge);
		
		drawRidge(ridge);	
	}
	
	this.removeRidge = function(ridge)
	{
		assert(ridge && ridge.id in annotations.ridges && ridge.id in allAnnotations);
		delete annotations.ridges[ridge.id];
		delete allAnnotations[ridge.id];
		
		clearRidge(ridge);
		
		onAnnotationRemoved(ridge);
	}

	this.addArea = function(area)
	{
		assert(area && !(area.id in annotations.areas) && !(area.id in allAnnotations));
		annotations.areas[area.id] = area;
		allAnnotations[area.id] = ['area', area];
		
		onAnnotationAdded(area);
		
		drawArea(area);	
	}
	
	this.removeArea = function(area)
	{
		assert(area && area.id in annotations.areas && area.id in allAnnotations);
		delete annotations.areas[area.id];
		delete allAnnotations[area.id];
		
		clearArea(area);
		
		onAnnotationRemoved(area);
	}

	this.addComment = function(comment)
	{
		assert(comment && !(comment.id in annotations.comments) && !(comment.id in allAnnotations));
		annotations.comments[comment.id] = comment;
		allAnnotations[comment.id] = ['comment', comment];
		
		onAnnotationAdded(comment);
		
		drawComment(comment);
	}
	
	this.removeComment = function(comment)
	{
		assert(comment && comment.id in annotations.comments && comment.id in allAnnotations);
		delete annotations.comments[comment.id];
		delete allAnnotations[comment.id];
		
		clearComment(comment);
		
		onAnnotationRemoved(comment);
	}

	this.updateAnnotation = function(annotation, values)
	{
		assert(annotation && annotation.id in allAnnotations);
		for (var key in values)
		{
			// ID cannot be changed. Pairing can only be changed through updateMinutiaPairing()
			// Creation and modification dates are also protected.
			// All other fields are mutable through this function.
			assert(key in annotation && key != 'pairing' && key != 'id' && key != 'created' && key != 'modified');
			annotation[key] = values[key];
		}
		
		var type = self.getAnnotationType(annotation.id);
		clearAnnotation(type, annotation);
		drawAnnotation(type, annotation);
		
		if (annotation.id in selection)
		{
			// This will clear and redraw the gizmos that were associated with the annotation.
			// It is the only way we can make this happen, even if it's overkill when only
			// non-visual properties were modified.
			self.selectAnnotation(annotation);
		}
	}
	
	this.getAnnotation = function(type, id)
	{
		assert(type in annotGroupByContent && annotGroupByContent[type] in annotations);
		var dict = annotations[annotGroupByContent[type]];
		assert(dict && id in dict);
		return dict[id];
	}
	
	this.getAllAnnotations = function()
	{
		return allAnnotations;
	}
	
	this.getAnnotationById = function(id)
	{
		assert(id in allAnnotations);
		return allAnnotations[id];
	}
	
	this.getAnnotationType = function(id)
	{
		assert(id in allAnnotations);
		return allAnnotations[id][0];
	}

	this.getPairedMinutia = function(pairId)
	{
		for (var i in annotations.minutiae)
		{
			var m = annotations.minutiae[i];
			if ('pairing' in m)
			{
				if (m.pairing == pairId)
					return m;
			}
		}
		return null;
	}
	
	this.getData = function(typeFilter, filterCallback)
	{
		var data;
		if (!typeFilter)
		{
			data = $.extend({}, annotations);
		}
		else
		{
			data = {};
			data[typeFilter] = $.extend({}, annotations[typeFilter]);
		}
		if (filterCallback)
		{
			var newData = {};
			for (var i in data)
			{
				var newMember = {};
				for (var j in data[i])
				{
					if (filterCallback(data[i][j], self))
						newMember[j] = data[i][j];
				}
				newData[i] = newMember;
			}
			data = newData;
		}
		data.image = params.image;
		return data;
	}
	
	this.setData = function(data, reassignIDs)
	{
		var filter = function(annotation) 
		{
			if (reassignIDs)
			{
				var newID = Pianos4Annotation._s_ids++;
				annotation.oldid = annotation.id;
				annotation.id = newID;
			}
		}

		if ('minutiae' in data)
		{
			for (var id in data.minutiae)
			{
				var m = data.minutiae[id];
				filter(m);
				self.addMinutia(m);
			}
		}
		if ('ridges' in data)
		{
			for (var id in data.ridges)
			{
				// The ridge case is special (see pianos4_annotation.js) but I'm not sure
				// it should be kept this way. As you see around for minutiae and areas, the
				// object we're creating here (in setData()) is *not* an "annotation" as the
				// interface creates it - it is merely a JSON (methodless) object. Since they
				// are equivalent (all fields of the annotation object are present in the JSON 
				// object) they are not distinguishable from the outside. This works fairly well
				// for other features, so why the ridge is treated differently, I don't know.
				// TODO: inspect and remove the "new Pianos4Ridge" thing if this is in fact old
				// code that should be refactored. 
				// HINT: look at the V3 compatibility layer (if this was meant to handle the poor
				// data format of the V3, then things should be fixed before the object lands here
				// DECISION: remove the constructor, things like v3 may break.
				var r = data.ridges[id];
				filter(r);
				self.addRidge(r);
			}
		}
		if ('areas' in data)
		{
			for (var id in data.areas)
			{
				var a = data.areas[id];
				filter(a);
				self.addArea(a);
			}
		}
		if ('comments' in data)
		{
			for (var id in data.comments)
			{
				var c = data.comments[id];
				filter(c);
				self.addComment(c);
			}
		}
	}
	
	this.clear = function()
	{
		for (var i in layers)
		{
			var layer = layers[i];
			if (layer.classed('pianos4_helpers'))
				continue; // Do not remove this as this is not related to data, but to tools (which assume they are in control of this node, not us)
			layers[i].selectAll('g').remove();
			layers[i].selectAll('text').remove();
		}
		for (var i in annotations)
			annotations[i] = {};
		allAnnotations = {};
		selection = {};
	}
	
	this.toggleLayer = function(layer, force)
	{
		assert(layer in layers);
		var l = layers[layer];

		var show;
		if (typeof(force) == 'boolean')
			show = force;
		else
			show = l.style('display') && l.style('display') == 'none';
		
		if (show)
			l.style('display',null);
		else
			l.style('display','none');
		
		return show;
	}
	
	this.pickByNode = function(node)
	{
		var ret = null;
		var n = node;
		var originalNode = node;
		
		// This loop climbs up the hierarchy until it reaches something "selectable" - ie
		// an SVG node with 'pianos4_editable' class. Recursion stops at the root svg node.
		while (n && n.nodeName != 'svg')
		{
			// "Correct" browsers - although in the realm of shitty standards, correct is a 
			// word one should use with caution - will return a wrapper object when clicking
			// a <use> node. This wrapper contains a reference to the instance (the <use> node
			// part of the SVG content) and to the "prototype" (the <svg:XXX> node part of the
			// <defs> of the SVG), in 'correspondingUseElement' and 'correspondingElement',
			// respectively. Firefox returns only the correspondingUseElement as the picked node.
			// This check aims at providing us with a 'node' parameter always set to the 
			// corresponding <use> element - which is the one we are picking.
			// Bonus question: which is right ? Hint: IE, Opera, Safari and Chrome behave the same.
			//                 Firefox is the only one behaving its way. Can IE be possibly right ?
			if (n.correspondingUseElement) 
			{
				n = n.correspondingUseElement;
			}
						
			// OTOH Firefox provides us with the very handy classList object, and others do not.
			// svgHasClass() abstracts this operation.
			if (svgHasClass(n, 'pianos4_editable'))
			{
				if (svgHasClass(n, 'pianos4_minutia'))
				{
					ret = {type:'minutia'};
				}
				else if (svgHasClass(n, 'pianos4_ridge'))
				{
					ret = {type:'ridge'};
				}
				else if (svgHasClass(n, 'pianos4_area'))
				{
					ret = {type:'area'};
				}
				else if (svgHasClass(n, 'pianos4_comment'))
				{
					ret = {type:'comment'};
				}
				else if (svgHasClass(n, 'pianos4_pair'))
				{
					// The pairing number shown in the GUI is actually in another layer than the
					// minutia itself - the 'redir' node is the redirection to the actual minutia
					// object in the minutiae layer.
					ret = {type:'minutia', id: parseInt(n.id.split('_')[1]) };
					var redir = d3svg.select('#'+ret.type+'_'+ret.id);
					assert(redir.length == 1);
					ret.node = redir.node();
				}
				if (ret) 
					break;
			}
			
				
			if (n.parentNode)
				n = n.parentNode;
			else
				n = null;
		}
		if (ret)
		{
			assert(ret.type);
			if (!('node' in ret))
				ret.node = n;
			ret.originalNode = 'correspondingUseElement' in originalNode ? originalNode.correspondingUseElement : originalNode;
			if (!('id' in ret))
				ret.id = parseInt(ret.node.id.substr(ret.type.length+1));
			ret.annotation = self.getAnnotation(ret.type, ret.id);
		}
		return ret;
	}

	this.selectAnnotation = function(annotations, discardOthers, noEvent)
	{
		if (discardOthers)
		{
			for (var aid in selection)
			{
				self.unselectAnnotation(aid, true, true);
			}
			selection = {};
		}
		
		if (annotations)
		{
			if (!$.isArray(annotations))
				annotations = [annotations];
			
			for (var k in annotations)
			{
				var annotation = annotations[k];

				assert(annotation.id in allAnnotations);
				selection[annotation.id] = true;
				
				var type = self.getAnnotationType(annotation.id);
				
				var node;
				if (params.useOtherFeaturesLayer && type == 'area' && 'type' in annotation && annotation.type == 'wcs')
					node = d3svg.select('.pianos4_others #'+type+'_'+annotation.id); // This is hackish, but anyway the association between feature type and layers must be rethinked.
				else
					node = d3svg.select('.pianos4_'+annotGroupByContent[type]+' #'+type+'_'+annotation.id);
				assert(!node.empty());
				
				if (type == 'minutia')
				{
					assert(node.length == 1 && node.select('use._gizmo').empty());
					var use = node.select('use').attr('href');
					node.append('svg:use')
						.classed('_rot', true)
						.attr('xlink:href', use+'_gizmo')
						.style('visibility','hidden')
						.attr('transform','scale(2)')
						.attr('pointer-events','all');
					node.append('svg:use')
						.classed('_gizmo', true)
						.attr('xlink:href', use+'_gizmo')
						.attr('stroke-width', 1.0)
						.attr('fill-opacity', 0.3);
				}
				else if (type == 'ridge' || type == 'area')
				{
					var svgridge = node.select('path');
					var gizmoWidth = parseFloat(svgridge.attr('stroke-width'))+4;
					node.insert('svg:path', 'path')
						.classed('_gizmo', true)
						.attr('stroke', svgridge.attr('stroke'))
						.attr('stroke-width', gizmoWidth)
						.attr('stroke-opacity', parseFloat(svgridge.attr('stroke-opacity') ? svgridge.attr('stroke-opacity')/2. : 0.5))
						.attr('fill','none')
						.attr('d', svgridge.attr('d'));
					
					for (var i in annotation.points)
					{
						var p = annotation.points[i];
						node.append('svg:circle')
							.classed('_handle', true)
							.attr('id', 'ridge_'+annotation.id+'_'+i)
							.attr('stroke', 'black')
							.attr('stroke-width', 1)
							.attr('fill', 'white')
							.attr('cx', p[0])
							.attr('cy', p[1])
							.attr('r', gizmoWidth/2.0+1.0);
					}
				}
			}
		}
		
		if (typeof(noEvent) == 'boolean' && noEvent)
			return;
		
		pianos.onSelectionChanged(self);
	}
	
	this.unselectAnnotation = function(aid, noEvent, noRemove)
	{
		var type = self.getAnnotationType(aid);
		var annotation = allAnnotations[aid][1];
		var node;
		if (params.useOtherFeaturesLayer && type == 'area' && 'type' in annotation && annotation.type == 'wcs')
			node = d3svg.select('.pianos4_others #'+type+'_'+annotation.id); // This is hackish, but anyway the association between feature type and layers must be rethinked.
		else
			node = d3svg.select('.pianos4_'+annotGroupByContent[type]+' #'+type+'_'+annotation.id);
		assert(!node.empty());
		assert(node.length == 1);
		assert(node.select('._gizmo').length == 1);
		node.select('._gizmo').remove();
		
		if (type == 'minutia') {
			assert(node.select('use._rot').length == 1);
			node.select('use._rot').remove();
		}
		else if (type == 'ridge' || type == 'area') {
			node.selectAll('._handle').remove();
		}
		
		if (typeof(noRemove) == 'undefined' || !noRemove)
			delete selection[aid];
		
		if (typeof(noEvent) == 'boolean' && noEvent)
			return;
		
		pianos.onSelectionChanged(self);
	}
	
	this.getSelection = function()
	{
		return selection;
	}
	
	this.getSelectionSize = function()
	{
		var count = 0;
		for (var i in selection)
			++count;
		return count;
	}
	
	this.deleteSelection = function()
	{
		var action = null;
		for (aid in selection)
		{
			if (!action)
				action = new Pianos4Action_Batch();
			var rem = new Pianos4Action_DeleteAnnotation(self, aid);
			action.append(rem);
		}
		if (action)
		{
			//self.selectAnnotation(null, true);
			pianos.exec(action);
		}
	}
	
	this.isSelected = function(annotation)
	{
		return annotation.id in selection;
	}
	
	this.showImageEditor = function()
	{
		assert(params.imageEditorScript);
		
		var old = $('.pianos4_image_editor');
		if (old.length)
			old.remove();
			
		var id = 'pianos4_'+params.type+'_editor_'+(new Date().getTime());
		var div = $('<div class="pianos4_image_editor" />');
		
		pianos.dialog(div, {
			title: 'Image Editor: '+params.type,
			autoOpen: false,
			width: 800,
			height: 600,
			modal: true,
			buttons: {
				'OK': function() {
					$(this).dialog('close');
					var action = new Pianos4Action_EditImage(self, editor.getFilters());
					pianos.exec(action);
				},
				'Cancel': function() {
					$(this).dialog('close');
				},
			},
			close: function()
			{
				// assert(editor);
				// editor.destroy();
				// delete editor;
				// editor = null;
			},
		});
		
		var content = $('<div id="'+id+'" />')
			.appendTo(div);
		
		var editor = new ImageEditor({
			divid: id,
			image: params.image,
			serverScript: params.imageEditorScript,
		});
		editor.init(filters);
			
		div.dialog('open');
		editor.pubLayout();
	}
	
	this.getImageFilters = function()
	{
		return filters;
	}
	
	this.setImageFilters = function(newFilters)
	{
		assert(params.imageEditorScript);

		var url;
		if (newFilters && newFilters.length > 1)
			url = params.imageEditorScript + '?' + ImageEditor.buildQueryString(newFilters);
		else
			url = params.image.href;

		filters = newFilters;
		layers.images.select('image').attr('xlink:href', url);
	}
	
	this.setMinutiaMarkerSize = function(factor)
	{	
		var items = ['minutia_bifurcation', 'minutia_ridge_ending', 'minutia_unknown', 'minutia_unknown2', 'minutia_center', 'minutia_difference'];
		
		var defs = d3svg.select('defs');
		
		var suffixes = ['', '_alt'];
		for (var s in suffixes)
		{
			var suffix = suffixes[s];
			for (var i in items)
			{
				var marker = defs.select('#'+items[i]+suffix);
				assert(marker.length == 1);
				var gizmo = defs.select('#'+items[i]+suffix+'_gizmo');
				assert(gizmo.length == 1);

				if (factor == 1.0)
				{
					// Just to keep things clean, remove the transform attribute
					// when the factor is 1 - it's a no-op anyway.
					marker.node().removeAttribute('transform');
					gizmo.node().removeAttribute('transform');
				}
				else
				{
					marker.attr('transform', 'scale('+factor+')');
					gizmo.attr('transform', 'scale('+factor+')');
				}
			}
		}
	}
	
	this.setSegments = function(segs)
	{
		layers.segments.selectAll('g').remove();
		segments = segs;
		if (segments)
			drawSegments();
	}

	this.setUserResultsVisible = function(userID, visible)
	{
		assert(params.splitByUser);
		$(['minutiae','ridges','areas','comments','others','pairing']).each(function() {
			if (!(this in layers))
				return;
			var layer = layers[this];
			var g = layer.select('g#'+this+'_user_'+userID);
			if (!g.empty())
			{
				if (visible)
					g.style('display', null);
				else
					g.style('display', 'none');
			}
		});
		//var g = layers
	}
	
	this.addH5CanvasCommand = function(command)
	{
		assert(params.useH5Canvas);
		
		h5CanvasCommands.push(command);
		drawH5CanvasCommand(command);
	}
	
	// Sets whether new annotations should be added to the SVG but kept hidden
	// by setting the user's parent node to 'display=none'. This can only be
	// used when params.splitByUser is true - otherwise we would have to either
	// hide every single annotation OR hide complete layers.
	// This does NOT show/hide existing annotations and will only work for new
	// per-user svg:g nodes. Use it before and after calling a batch of this.setData().
	// See pianos.setData().
	this.setNewAnnotationsHidden = function(hidden)
	{
		assert(params.splitByUser);
		hideNewAnnotations = hidden;
	}
	
	// ------------------------------
	// Private variables
	// ------------------------------
	
	var self = this;
	var d3svg = null;
	var layers = null;
	var zoom = 1.0;
	var annotations = {
		minutiae: {},
		ridges: {},
		areas: {},
		comments: {},
	};
	var allAnnotations = {};
	var annotGroupByContent = {};
	var capturingTool = null;
	var origin = [0,0];
	var originLocked = false;
	var temporaryTool = null;
	var selection = {};
	var filters = null;
	var segments = null;
	var h5Canvas = null;
	var h5CanvasCommands = [];
	var hideNewAnnotations = false;
	
	
	// ------------------------------
	// Private functions
	// ------------------------------
	

	var buildUI = function()
	{
		params.div
			.addClass('pianos4_view')
			.data('Pianos4View', self);
		
		var canvas = $('<div class="pianos4_view_canvas"></div>');
		params.div.append(canvas);
		
		d3svg = d3.select(params.path + ' .pianos4_view_canvas')
			.append("svg:svg")
			//.attr("class", "chart")
			.attr("width", canvas.width())
			.attr("height", canvas.height());
			
		// SVG Definitions
		var defs = d3svg.append('svg:defs');
		var mgizmo_color = '#00aa00';

		var minutiaData = [
			{suffix: '', color: '#ff0000'},
			{suffix: '_alt', color: '#ffff00'},
		];
		
		for (var minDataIndex in minutiaData)
		{
			var minData = minutiaData[minDataIndex];
			
			// Bifurcation minutia marker
			var bif = defs.append('svg:g')
				.attr('id', 'minutia_bifurcation'+minData.suffix);
			// White outline
			bif.append('svg:path')
				.attr('d', 'M 10 0 L 10 -10 L -10 -10 L -10 10 L 10 10 L 10 0 L 50 0')
				.attr('stroke-width', 4)
				.attr('stroke', '#ffffff')
				.attr('fill', 'none');
			// Colored marker
			bif.append('svg:path')
				.attr('d', 'M 10 0 L 10 -10 L -10 -10 L -10 10 L 10 10 L 10 0 L 50 0')
				.attr('stroke-width', 2)
				.attr('stroke', minData.color)
				.attr('fill', 'none');
			// Selectable area (not visible)
			var bif_gizmo = defs.append('svg:rect')
				.attr('id', 'minutia_bifurcation'+minData.suffix+'_gizmo')
				.attr('x', -15)
				.attr('y', -15)
				.attr('width', 70)
				.attr('height', 30)
				.attr('stroke', mgizmo_color)
				//.attr('stroke-width', 0.0)  // Must not be present so as to be overridable
				.attr('fill', mgizmo_color);
				//.attr('fill-opacity', 0.0);
			
			// Ridge ending minutia marker
			var re = defs.append('svg:g')
				.attr('id', 'minutia_ridge_ending'+minData.suffix);
			// White outline
			re.append('svg:circle')
				.attr('cx', 0)
				.attr('cy', 0)
				.attr('r', 12)
				.attr('stroke-width', 4)
				.attr('stroke', '#ffffff')
				.attr('fill', 'none');
			re.append('svg:path')
				.attr('d', 'M 12 0 L 50 0')
				.attr('stroke-width', 4)
				.attr('stroke', '#ffffff')
				.attr('fill', 'none');
			// Colored marker
			re.append('svg:circle')
				.attr('cx', 0)
				.attr('cy', 0)
				.attr('r', 12)
				.attr('stroke-width', 2)
				.attr('stroke', minData.color)
				.attr('fill', 'none');
			re.append('svg:path')
				.attr('d', 'M 12 0 L 50 0')
				.attr('stroke-width', 2)
				.attr('stroke', minData.color)
				.attr('fill', 'none');
			// Selectable area (not visible)
			var re_gizmo = defs.append('svg:rect')
				.attr('id', 'minutia_ridge_ending'+minData.suffix+'_gizmo')
				.attr('x', -15)
				.attr('y', -15)
				.attr('width', 70)
				.attr('height', 30)
				.attr('stroke', mgizmo_color)
				//.attr('stroke-width', 0.0)
				.attr('fill', mgizmo_color);
				//.attr('fill-opacity', 0.0);
			
			// Unknown minutia marker
			var unk = defs.append('svg:g')
				.attr('id', 'minutia_unknown'+minData.suffix);
			// White outline
			unk.append('svg:path')
				.attr('d', 'M 10 0 L -12 -12 L -12 12 L 10 0 L 50 0')
				.attr('stroke-width', 4)
				.attr('stroke', '#ffffff')
				.attr('fill', 'none');
			// Colored marker
			unk.append('svg:path')
				.attr('d', 'M 10 0 L -12 -12 L -12 12 L 10 0 L 50 0')
				.attr('stroke-width', 2)
				.attr('stroke', minData.color)
				.attr('fill', 'none');
			// Selectable area (not visible)
			var unk_gizmo = defs.append('svg:rect')
				.attr('id', 'minutia_unknown'+minData.suffix+'_gizmo')
				.attr('x', -15)
				.attr('y', -15)
				.attr('width', 70)
				.attr('height', 30)
				.attr('stroke', mgizmo_color)
				//.attr('stroke-width', 0.0)
				.attr('fill', mgizmo_color);
				//.attr('fill-opacity', 0.0);
			
			// Unknown2 minutia marker
			var unk2 = defs.append('svg:g')
				.attr('id', 'minutia_unknown2'+minData.suffix);
			// White outline
			unk2.append('svg:path')
				.attr('d', 'M 20 0 L 0 -15 L -20 0 L 0 15 L 20 0 L 50 0')
				.attr('stroke-width', 4)
				.attr('stroke', '#ffffff')
				.attr('fill', 'none');
			// Colored marker
			unk2.append('svg:path')
				.attr('d', 'M 20 0 L 0 -15 L -20 0 L 0 15 L 20 0 L 50 0')
				.attr('stroke-width', 2)
				.attr('stroke', minData.color)
				.attr('fill', 'none');
			// Selectable area (not visible)
			var unk2_gizmo = defs.append('svg:rect')
				.attr('id', 'minutia_unknown2'+minData.suffix+'_gizmo')
				.attr('x', -15)
				.attr('y', -15)
				.attr('width', 70)
				.attr('height', 30)
				.attr('stroke', mgizmo_color)
				//.attr('stroke-width', 0.0)
				.attr('fill', mgizmo_color);
				//.attr('fill-opacity', 0.0);

			// Center minutia marker
			var center = defs.append('svg:g')
				.attr('id', 'minutia_center'+minData.suffix);
			// White outline
			center.append('svg:circle')
				.attr('cx', 0)
				.attr('cy', 0)
				.attr('r', 16)
				.attr('stroke-width', 6)
				.attr('stroke', '#ffffff')
				.attr('fill', 'none');
			center.append('svg:line')
				.attr('x1', -16)
				.attr('y1', 0)
				.attr('x2', 16)
				.attr('y2', 0)
				.attr('stroke-width', 4)
				.attr('stroke', '#ffffff')
				.attr('fill', 'none');
			// Colored marker
			center.append('svg:circle')
				.attr('cx', 0)
				.attr('cy', 0)
				.attr('r', 16)
				.attr('stroke-width', 4)
				.attr('stroke', minData.color)
				.attr('fill', 'none');
			center.append('svg:line')
				.attr('x1', -16)
				.attr('y1', 0)
				.attr('x2', 16)
				.attr('y2', 0)
				.attr('stroke-width', 2)
				.attr('stroke', minData.color)
				.attr('fill', 'none');
			// Selectable area (not visible)
			var center_gizmo = defs.append('svg:circle')
				.attr('id', 'minutia_center'+minData.suffix+'_gizmo')
				.attr('cx', 0)
				.attr('cy', 0)
				.attr('r', 20)
				.attr('stroke', mgizmo_color)
				//.attr('stroke-width', 0.0)
				.attr('fill', mgizmo_color);
				//.attr('fill-opacity', 0.0);
			
			// Difference minutia marker -  a 5-branch star
			var radius1 = 16;
			var radius2 = 8;
			var branches = 5;
			var angle0 = Math.PI/2.0;
			var outer = [], inner = [];
			for (var i=0;i<branches;++i)
			{
				var angle = angle0 + 2.0*Math.PI/branches*i;
				var p = [radius1*Math.cos(angle), radius1*Math.sin(angle)];
				outer.push(p);
				angle = angle0 + 2.0*Math.PI/branches*(i+0.5);
				p = [radius2*Math.cos(angle), radius2*Math.sin(angle)];
				inner.push(p);
			}
			var diffpath = 'M ';
			for (var i=0;i<branches;++i)
			{
				if (i)
					diffpath += ' L ';
				diffpath += outer[i][0] + ' ' + outer[i][1];
				if (i)
					diffpath += ' L ';
				diffpath += inner[i][0] + ' ' + inner[i][1];
			}
			diffpath += ' Z';
			var diff = defs.append('svg:g')
				.attr('id', 'minutia_difference'+minData.suffix);
			// White outline
			diff.append('svg:path')
				.attr('d', diffpath)
				.attr('stroke-width', 4)
				.attr('stroke', '#ffffff')
				.attr('fill', 'none');
			// Colored marker
			diff.append('svg:path')
				.attr('d', diffpath)
				.attr('stroke-width', 2)
				.attr('stroke', minData.color)
				.attr('fill', 'none');
			// Selectable area (not visible)
			var diff_gizmo = defs.append('svg:circle')
				.attr('id', 'minutia_difference'+minData.suffix+'_gizmo')
				.attr('cx', 0)
				.attr('cy', 0)
				.attr('r', radius1+4)
				.attr('stroke', mgizmo_color)
				//.attr('stroke-width', 0.0)
				.attr('fill', mgizmo_color);
				//.attr('fill-opacity', 0.0);
		}

		// Actual content of the SVG

		// Root transform node
		var root = d3svg.append('svg:g').classed('pianos4_root', true);
		//root.attr('transform', 'rotate(30 335.54896862471435 471.5256302848967)');

		// Layers
		layers = {
			images: root.append('svg:g').attr('class', 'pianos4_layer pianos4_images'),
			helpers: root.append('svg:g').attr('class', 'pianos4_layer pianos4_helpers'),
			areas: root.append('svg:g').attr('class', 'pianos4_layer pianos4_areas'),
			others: root.append('svg:g').attr('class', 'pianos4_layer pianos4_others'),
			ridges: root.append('svg:g').attr('class', 'pianos4_layer pianos4_ridges'),
			minutiae: root.append('svg:g').attr('class', 'pianos4_layer pianos4_minutiae'),
			segments: root.append('svg:g').attr('class', 'pianos4_layer pianos4_segments'),
			pairing: root.append('svg:g').attr('class', 'pianos4_layer pianos4_pairing'),
			comments: d3svg.append('svg:g').attr('class', 'pianos4_layer pianos4_comments'),
		};
		self.toggleLayer('pairing', false);
		self.toggleLayer('segments', false);
		
		// Finger-print/mark Image
		var image = layers.images.append('svg:image')
			.attr('x', 0)
			.attr('y', 0)
			.attr('width', params.image.width+'px')
			.attr('height', params.image.height+'px')
			.attr('xlink:href', params.image.href);
			
		// Event handlers
		d3svg.on('click', function(data,index) { 
				// All SVG elements with xlink:hrefs are not browseable targets - so we should
				// not "open in a new window" on ctrl+click or middle-click. The tools use the
				// 'mouse[down/move/up]' events anyway so we can disable the 'click' event overall.
				// Note that this does the trick on IE, but not in Firefox, where there seems to
				// be no way of preventing the middle-click from opening a window - ctrl+click is
				// disabled, though.
				d3.event.preventDefault();
			}).on('mouseup', function() { d3.event.preventDefault(); });
			
		d3svg.on('mousedown', function(data,index) { 
			// Delegate all mouse events to the tool management subsystem
			d3.event.preventDefault(); 
			onMouseDown(d3.event); 
		});
		canvas.bind('mousewheel', onMouseWheel);
		pianos.registerHandler(['keydown', 'space'], onKeyDown).registerHandler(['keyup', 'space'], onKeyUp);
		pianos.registerHandler(['keydown', params.editToolHotkey], onKeyDown).registerHandler(['keyup', params.editToolHotkey], onKeyUp);
		pianos.registerHandler(['keydown', 'del'], onKeyDown).registerHandler(['keyup', 'del'], onKeyUp);
		pianos.registerHandler(['keydown', 'backspace'], onKeyDown).registerHandler(['keyup', 'backspace'], onKeyUp);

		canvas.bind('contextmenu', function(e) {
			onContextMenu();
		});
		
		if (params.useH5Canvas)
		{
			h5Canvas = $('<canvas />').addClass('pianos4_view_h5canvas pianos4_view_canvas');
			params.div.append(h5Canvas);
			
			h5CanvasCommands.push(function(ctx) { ctx.fillRect(10,10,50,50); });
		}
	}
	
	var onContextMenu = function()
	{
	}
	
	var onMouseMove = function(event, ui)
	{
		assert(capturingTool);
		
		if (capturingTool.onMouseMove(self.getCanvasPos([event.clientX, event.clientY]), event.target, event))
			releaseMouse();
	}
	
	var onMouseUp = function(event, ui)
	{
		assert(capturingTool);

		if (capturingTool.onMouseUp(self.getCanvasPos([event.clientX, event.clientY]), event.target, event))
			releaseMouse();
	}
	
	var captureMouse = function(tool) 
	{
		assert(!capturingTool);
		capturingTool = tool;
		capturingTool.isBusy = true;
		
		log('Starting tool '+capturingTool.getName());

		$(document).bind('mousemove', onMouseMove).bind('mouseup', onMouseUp);
		capturingTool.bind(self);

		pianos.onMouseCaptured(capturingTool);
	}
	
	var releaseMouse = function() 
	{
		assert(capturingTool);
		$(document).unbind('mousemove', onMouseMove).unbind('mouseup', onMouseUp);
		capturingTool.unbind();
		capturingTool.isBusy = false;

		log('Ended tool '+capturingTool.getName());

		// Defer callbacks to after the internal state was fully updated
		var tmp = capturingTool;
		capturingTool = null;

		pianos.onMouseReleased(tmp);
	}
	
	// Note: unlike onMouseUp() and onMouseMove(), which are jQuery event handlers,
	//       this function is a d3 event handler.
	var onMouseDown = function(event)
	{	
		if (event.button >= 2)
		{
			if (capturingTool)
			{
				// Right-clicking when using a tool means 'cancel' the 
				// current move (or stop it e.g. for a pan) ...
				if ('cancel' in capturingTool)
				{
					if (capturingTool.cancel())
						return; // ... unless the tool refuses the cancel() command
						
					pianos.skipNextContextMenu();
				}
				releaseMouse();
				return;
			}
			else
			{
				// Otherwise the right click first acts as a click with the 'edit' tool
				// (to select the object under the cursor) and then pops up the context
				// menu for this object.

				// In DEBUG mode, however, the Control key must be pressed for the menu
				// to be shown - this lets the usual right-click show the browser's
				// context menu, including its very handy 'Inspect element' entry.
				if (params.debug && !event.ctrlKey)
					return;

				pianos.skipNextContextMenu();
				captureMouse(pianos.getTool('edit'));
				
				// var picked = self.pickByNode(event.target);
				
					
				// log(event);
				// log(picked);
				// log('First intercept');
				// //event.preventDefault();
				// //event.stopPropagation();
			}
		}

		if (event.button == 1)
		{
			if (capturingTool)
				return;
				
			// Middle mouse button drags result in a temporary 'pan' tool selection
			captureMouse(pianos.getTool('pan'));
		}
		
		if (!capturingTool)
		{
			//log(event.target);
			//if (event.target.tagName != 'image')
			//	return;

			var tool;
			// If tool inference is activated
			if (params.inferEditTool && !temporaryTool)
			{
				var active = pianos.getActiveTool();
				var activename = active ? active.getName() : '';
				// Inference is made inactive when using the pairing tool and grouping tool
				if (activename != 'pairing' && activename != 'group')
				{
					var picked = self.pickByNode(event.target);
					// If the user clicked an annotation (only a minutia, actually)
					if (picked && picked.annotation && picked.type == 'minutia')
					{
						// Set the inferred tool to 'edit', unless we were pressing shift
						// so that close minutiae can be placed by reversing the functionality
						// of the "force edit tool" feature.
						if (activename != 'minutia' || !temporaryTool || temporaryTool.getName() != 'edit')
							tool = pianos.getTool('edit');
					}
				}
			}
			
			if (!tool)
				tool = temporaryTool ? temporaryTool : pianos.getActiveTool();
		
			if (!tool || tool.isBusy)
				return;
				
			captureMouse(tool);
		}

		assert(capturingTool);
		
		if (capturingTool.onMouseDown(self.getCanvasPos([event.clientX, event.clientY]), event.target, event))
			releaseMouse();
	}
	
	var onMouseWheel = function(event, delta, deltaX, deltaY)
	{
		var newFactor = 0.01*pianos.getNewZoomFactor(zoom*100.0, delta < 0);
		//log('(un)zooming from '+zoom+' to '+newFactor);
		self.setZoomFactor(newFactor, [event.clientX, event.clientY]);
		var anchorOffset = null;
		if (originLocked)
		{
			var originLockedUnproj = self.getAbsPos(originLocked);
			anchorOffset = [event.clientX-originLockedUnproj[0], event.clientY-originLockedUnproj[1]];
		}
		pianos.setOtherViewZoomFactor(self, newFactor, anchorOffset);
	}
	
	var onKeyDown = function(event)
	{
		if (event.data == 'backspace')
			event.preventDefault();
			
		if (!capturingTool)
		{
			if (event.data == 'space')
			{
				temporaryTool = pianos.getTool('pan');
				var views = pianos.getViews();
				for (var i in views)
					views[i].getSVG().classed('pianos4_pan_tool_on', true);
			}
			else if (event.data == params.editToolHotkey && pianos.getActiveTool())
			{
				temporaryTool = pianos.getTool('edit');
			}
			else if (event.data == 'del' || event.data == 'backspace')
			{
				if (pianos.getViewWithFocus() 
					&& pianos.getViewWithFocus().getType() == params.type
					&& pianos.isReady())
				{
					self.deleteSelection();
				}
			}
		}
	}
	
	var onKeyUp = function(event)
	{
		if (temporaryTool)
		{
			temporaryTool = null;
			var views = pianos.getViews();
			for (var i in views)
				views[i].getSVG().classed('pianos4_pan_tool_on', false);
		}
	}
	
	
	
	var onAnnotationAdded = function(annotation)
	{
		if (!annotation.created)
		{
			var d = new Date();
			annotation.created = d;
			annotation.modified = d;
		}
		else
		{
			// If this annotation comes from the DB, the created and modified
			// fields are strings, rather than Date objects. Convert them back.
			if (typeof(annotation.created) == 'string')
				annotation.created = new Date(pgTimestamptzToDate(annotation.created));
			if (typeof(annotation.modified) == 'string')
				annotation.modified = new Date(pgTimestamptzToDate(annotation.modified));
		}

		if (!('userid' in annotation))
			annotation.userid = pianos.getUser().id;
		
		if (annotation.id >= Pianos4Annotation._s_ids)
			Pianos4Annotation._s_ids = annotation.id + 1;
		
		if ('pairing' in annotation && annotation.pairing >= Pianos4Annotation._s_pairIds)
			Pianos4Annotation._s_pairIds = annotation.pairing + 1;
			
		pianos.clearSelection();
	}
	
	var onAnnotationRemoved = function(annotation)
	{
	}

	// Since the various annotations can be stored in two ways:
	// 1) grouped by type (params.splitByUser = false)
	// 			annotation type X layer 
	//				annotation X1
	//              annotation X2
	// 			annotation type Y layer 
	//				annotation Y1
	//              annotation Y2
	// 2) grouped by type and by user (params.splitByUser = true)
	// 			annotation type X layer 
	//				user 1
	//					annotation U1_X1
	//              	annotation U1_X2
	//				user 2
	//					annotation U2_X1
	//              	annotation U2_X2
	// 			annotation type Y layer 
	//				... (you get the point)
	// this function returns the correct svg:g parent to insert the given annotation.
	// In situation 1) it will return the 'annotation type X layer'
	// In sutation 2) it will return the user's group inside the layer
	// Note: we need the layer because the annotation itself does not have a type (?!)
	var getDrawableGroup = function(layer, annotation, returnParent)
	{
		assert(layer in layers);
		var parent = null;
		if (params.splitByUser)
		{
			assert('userid' in annotation);
			var g = layers[layer].select('g#'+layer+'_user_'+annotation.userid);
			if (g.empty())
			{
				g = layers[layer].append('svg:g')
					.attr('id', layer+'_user_'+annotation.userid);
					
				if (hideNewAnnotations)
					g.style('display','none');
			}
			parent = g;
		}
		else
		{
			parent = layers[layer];
		}
		assert(parent);
		if (returnParent)
			return [parent, parent.append('svg:g')];
		else
			return parent.append('svg:g');
	}
	
	var drawMinutia = function(minutia)
	{
		var g = getDrawableGroup('minutiae', minutia)
			.attr('transform', 'translate('+minutia.pos[0]+','+minutia.pos[1]+') rotate('+minutia.angle+')')
			.attr('id', 'minutia_'+minutia.id)
			.attr('class', 'pianos4_editable pianos4_minutia');
		
		// Decide whether we're drawing this minutiae in red (regular minutia) or in 
		// yellow (alternative minutia, used when the minutia has been added or modified 
		// during the comparison phase)
		var altSuffix = '';
		if (params.showModifiedMinutiae)
		{
			if (params.updatedMinutiaeTimestamp && minutia.modified > params.updatedMinutiaeTimestamp)
			{
				// We have a global date (given in the constructions parameters) to compare against: if
				// the minutia is more recent, use the alternative color.
				altSuffix = '_alt';
			}
			else if ('userid' in minutia && pianos.getUser().tutor)
			{
				// In tutor review mode, we do not have a global value because each user has a
				// different analysis submission date. We ask the application for the user, and 
				// provided the annotation comes from a known user (and not the tutor playing around)
				// we check the user's submission date against the minutia's modification date.
				
				// 'progress' is not a property of the user if it is the tutor currently reviewing the
				// results (although there can be tutors in the results set, but those are not logged
				// in and DO have a progress value set) -> don't do anything if this annotation belong
				// to the logged-in tutor.
				if (pianos.getUser().id != minutia.userid)
				{
					var user = pianos.getUser(minutia.userid);
					assert('progress' in user);
					// 'analysis_submitted_on' is a property of user.progress iff the status is >= 2, that
					// is, if the analysis has been submitted.
					if ('analysis_submitted_on' in user.progress && minutia.modified > user.progress.analysis_submitted_on)
						altSuffix = '_alt';
				}
			}
		}
		
		// The minutia marker itself
		var min = g.append('svg:use')
				.attr('xlink:href', '#minutia_'+minutia.type+altSuffix);
		// A transparent (invisible) background just for the purpose of easier selection
		g.append('svg:use')
			.classed('_sel', true)
			.attr('xlink:href', '#minutia_'+minutia.type+'_gizmo')
			.style('visibility', 'hidden')
			.attr('pointer-events', 'all') 
		//	.attr('stroke-width', 0.0)
		//	.attr('fill-opacity', 0.0)
		;
		//alert(g.attr('pointer-events'));
		
		if ('pairing' in minutia)
			self.updateMinutiaPairing(minutia);
	}
	
	var clearMinutia = function(minutia)
	{
		var node = layers.minutiae.select('#minutia_'+minutia.id);
		assert(node && node.length == 1);
		node.remove();
	}
	
	var drawRidge = function(ridge)
	{
		var def = 'M ';
		for (var i in ridge.points)
		{
			if (i>0)
				def += ' L';
			def += ' '+ridge.points[i][0]+' '+ridge.points[i][1];
		}
		
		var g = getDrawableGroup('ridges', ridge)
			.attr('id', 'ridge_'+ridge.id)
			.attr('class', 'pianos4_editable pianos4_ridge')
			.append('svg:path')
				.attr('stroke', ridge.color)
				.attr('stroke-width', ridge.width)
				.attr('stroke-opacity', ridge.opacity)
				.attr('fill','none')
				.attr('d', def);
	}
	
	var clearRidge = function(ridge)
	{
		var node = layers.ridges.select('#ridge_'+ridge.id);
		assert(node && node.length == 1);
		node.remove();
	}
	
	var drawArea = function(area)
	{
		var def = 'M ';
		for (var i in area.points)
		{
			if (i>0)
				def += ' L';
			def += ' '+area.points[i][0]+' '+area.points[i][1];
		}
		def += ' z';
		
		if ('holes' in area)
		{
			for (var h in area.holes)
			{
				def += ' M ';
				var hole = area.holes[h];
				for (var i in hole)
				{
					if (i>0)
						def += ' L';
					def += ' '+hole[i][0]+' '+hole[i][1];
				}
				def += ' z';
			}
		}
		
		var drawGroup;
		if (params.useOtherFeaturesLayer && area.type == 'wcs')
			drawGroup = 'others';
		else
			drawGroup = 'areas';
			
		var pg = getDrawableGroup(drawGroup, area, true);
		var g = pg[1]
			.attr('id', 'area_'+area.id)
			.attr('class', 'pianos4_editable pianos4_area');
		var path = g.append('svg:path')
			.attr('stroke', area.color)
			.attr('stroke-width', 5.0)
			.attr('fill', area.color)
			.attr('fill-opacity', 0.3)
			.attr('d', def);
		
		if ('type' in area && area.type == 'quality')
		{
			// Keep the quality layers ordered by their value
			g.data([area.value]); // d3 needs data to do the sort (otherwise a and b just below are undefined)
			pg[0].selectAll('g.pianos4_area').sort(function(a,b) {
				return a < b;
			});
		}
	}
	
	var clearArea = function(area)
	{
		var layer;
		if (params.useOtherFeaturesLayer && area.type == 'wcs')
			layer = layers.others;
		else
			layer = layers.areas;
		var node = layer.select('#area_'+area.id);
		assert(node && node.length == 1);
		node.remove();
	}
	
	var drawComment = function(comment)
	{
		var g = getDrawableGroup('comments', comment)
			.attr('id', 'comment_'+comment.id)
			.attr('class', 'pianos4_editable pianos4_comment');
		
		// Round the comment corner with a radius of 10. This will slightly change the point
		// where the line should be attached.
		var rx = 10;
		var lineGrow = Math.sqrt(2*rx*rx)-rx;
		var vn = [Math.sqrt(2)/2, Math.sqrt(2)/2];
		var end = [comment.boxpos[0]+vn[0]*lineGrow, comment.boxpos[1]+vn[1]*lineGrow];
		
		g.append('svg:line')
				.attr('stroke', comment.color)
				.attr('stroke-width', 2.0)
				.attr('x1', comment.pos[0])
				.attr('y1', comment.pos[1])
				.attr('x2', end[0])
				.attr('y2', end[1]);
				
		g.append('svg:rect')
				.attr('stroke', comment.color)
				.attr('stroke-width', 2.0)
				.attr('fill', comment.color)
				.attr('fill-opacity', 0.5)
				.attr('rx', rx)
				.attr('height', 100)
				.attr('width', 200)
				.attr('x', comment.boxpos[0])
				.attr('y', comment.boxpos[1]);
	}
	
	var clearComment = function(comment)
	{
		var node = layers.comments.select('#comment_'+comment.id);
		assert(node && node.length == 1);
		node.remove();
	}
	
	var drawAnnotation = function(type, annotation)
	{
		// The code below is a hack, and is further incompatible with JS minimization
		//var funcname = 'draw'+type.substr(0,1).toUpperCase()+type.substr(1);
		//eval(funcname)(annotation);
		
		// Hence the ugly (but semantically correct) following switch statement
		switch (type) {
			case 'minutia': drawMinutia(annotation); break;
			case 'ridge': drawRidge(annotation); break;
			case 'area': drawArea(annotation); break;
			case 'comment': drawComment(annotation); break;
			default: assert(false); break;
		}
	}

	var clearAnnotation = function(type, annotation)
	{
		// Same remark as above in drawAnnotation()
		switch (type) {
			case 'minutia': clearMinutia(annotation); break;
			case 'ridge': clearRidge(annotation); break;
			case 'area': clearArea(annotation); break;
			case 'comment': clearComment(annotation); break;
			default: assert(false); break;
		}
	}
	
	var drawSegments = function()
	{
		assert(segments);
		
		var base = self.isMark() ? 1 : 3;
		for (var i in segments)
		{
			var s = segments[i];
			
			var from = s[base];
			var to = s[base+1];
			
			assert(from in annotations.minutiae && to in annotations.minutiae);
			
			var m1 = annotations.minutiae[from];
			var m2 = annotations.minutiae[to];
			
			var g = layers.segments.append('svg:g')
				.attr('id', params.type+'_segment_'+s[0]);
			
			/*g.append('svg:line')
				.attr('x1', m1.pos[0])
				.attr('y1', m1.pos[1])
				.attr('x2', m2.pos[0])
				.attr('y2', m2.pos[1])
				.attr('stroke', '#ffffff')
				.attr('stroke-width', 4);*/
				
			g.append('svg:line')
				.attr('x1', m1.pos[0])
				.attr('y1', m1.pos[1])
				.attr('x2', m2.pos[0])
				.attr('y2', m2.pos[1])
				.attr('stroke', '#00cc00')
				.attr('stroke-width', 3);
		}
	}
	
	var drawH5Canvas = function()
	{
		assert(h5Canvas);
		var ctx = h5Canvas.get(0).getContext('2d');
		for (var i in h5CanvasCommands)
		{	
			var command = h5CanvasCommands[i];
			drawH5CanvasCommand(command, ctx);
		}
	}
	
	var drawH5CanvasCommand = function(command, ctx)
	{
		assert(h5Canvas);

		if (!ctx)
			ctx = h5Canvas.get(0).getContext('2d');
			
		if (typeof(command) == 'function')
		{
			command(ctx);
		}
		else if ('drawH5' in command)
		{
			command.drawH5(ctx);
		}
		else
		{
			assert(false);
		}
	}
}
