/* imageeditor.js */
/*
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 ImageEditor(params)
{
	// ------------------------------
	// Constructor
	// ------------------------------
	
	params = params || {};
	if (!params) params = new Object();
	if (!params.divid) params.divid = 'imageeditor';
	if (!params.idPrefix) params.idPrefix = params.divid+'_';
	if (!params.cssPrefix) params.cssPrefix = 'imageeditor_';
	if (!params.image) alert('No image given !');
	if (!params.serverScript) params.serverScript = 'php/imageeditor.php';
	
	// ------------------------------
	// Public methods
	// ------------------------------
	
	this.init = function(startFilters)
	{
		// This is not the first time we are init()ialized, so first unbind the
		// previous events (the instance pointed to by the previous handler(s) 
		// is probably not the same as 'self', as it makes no sense to init()
		// twice the same ImageEditor instance).
		if (ImageEditor.resizeEventHandler)
		{
			$(window).unbind('resize', ImageEditor.resizeEventHandler);
			ImageEditor.resizeEventHandler = null;
		}

		var toolsParams = {
			cssPrefix: params.cssPrefix,
		};
		
		toolsToInstall = [
			[new ImageEditor_BrightnessContrast(toolsParams), ['common']],
			[new ImageEditor_Invert(toolsParams), ['common']],
		];

		buildUI();

		addFilter({type:'original', label:'Original image', url: $('#'+params.divid+' .'+mycss('image_view')+' img').get(0).src});
		
		if (startFilters)
		{
			for (var i in startFilters)
			{
				// The first filter is the original image, and it's been inserted already
				if (i == 0)
					continue;

				var filter = $.extend({}, startFilters[i]);

				// The filter is OK for its data part.
				

				// The binding to the tools is not correct OTOH, so we need to recreate it. 
				// Note that the anonymous function that is triggered on click on the 
				// 'add filter' button does exactly this (duplicate code?)
				filter.tool = tools[filter.type];
				filter.label = filter.tool.getLabel();
			
				// Trick the system into believing this is the active tool
				activeTool = filter.type;
				// And create the filter
				addFilter(filter);
				
				// Note: as a (nasty) side-effect of doing addFilter(), selectFilterByIndex()
				// will be called, thus selecting the filter AND the associated options panel.
				// If this is not desired, some refactoring must take place. I don't care now.
			}
		}
		
		//self.setActiveTool('brightness_contrast');

		layout();
		
		
		ImageEditor.resizeEventHandler = function() {
			
			layout();
		};
		
		$(window).bind('resize', ImageEditor.resizeEventHandler);
	}

	this.setActiveTool = function(id, isLayerSelection)
	{
		var pane = $('.'+mycss('tools_options'));
		var buttons = views.tools.find('.'+mycss('tools_actions')+' a');
		
		if (activeTool)
		{
			 
			 tools[activeTool].getPanel().detach();
			 pane.html('');
		}
		
		//
		
		activeTool = id;

		if (activeTool)
		{
			
			
			var tool = tools[activeTool];
		
			var content = $('<div />');
			
			var helpMsg = tool.getHelp();
			var help1 = $('<div />')
				.addClass(mycss('tool_help')+' ui-state-default ui-corner-top ui-corner-bottom')
				.text(helpMsg[0])
				.append($('<a href="#">[more]</a>')
					.click(function(e) {
						e.preventDefault();
						var extended = $(this).parent().next();
						extended.toggle();
						if (extended.is(':visible'))
						{
							$(this).text('[less]');
							help1.removeClass('ui-corner-bottom');
						}
						else
						{
							$(this).text('[more]');
							help1.addClass('ui-corner-bottom');
						}
					})
				)
			;
			
			var help2 = $('<div />').addClass(mycss('tool_exthelp')+' ui-widget-content ui-corner-bottom').css('display','none');
			help2.append($('<span />').text(helpMsg[1]));
			content.append(help1).append(help2).append(tool.getPanel());
			
			pane.html(content);

			if (isLayerSelection)
			{
				buttons.filter('a[href=#add]').css('visibility','hidden');
				//buttons.filter('a[href=#update]').removeCSS('visibility');
			}
			else
			{
				buttons.filter('a[href=#add]').removeCSS('visibility');
				//buttons.filter('a[href=#update]').css('visibility','hidden');
			}
		}
		else
		{
			views.tools.find('.'+mycss('tools_actions')+' a').css('visibility','hidden');
			
			var showImageInfo = function()
			{
				
				pane.html(imageInfo.panel);
			}
			
			var updateImageInfo = function()
			{
				
				//
				//
				var panel;
				if (imageInfo.success)
				{
					panel = $('<pre />').text(imageInfo.ret);
				}
				else
				{
					panel = $('<div />');
					panel.append($('<h3 />').text('Could not retrieve detailed information about the image'));
					panel.append($('<p />').text(imageInfo.message));
				}
				imageInfo.panel = panel;
				
				if (!activeTool)
					showImageInfo();
			}
			
			if (imageInfo)
			{
				showImageInfo();
			}
			else
			{
				var absImageUrl = $('#'+params.divid+' .'+mycss('image_view')+' img').get(0).src;
				$.ajax({
					type: 'get',
					url: params.serverScript+'?action=info&url='+encodeURIComponent(absImageUrl),
					dataType: 'json',
					success: function(data,a,b) {
						imageInfo = data;
						updateImageInfo();
					},
					error: function(data) {
						imageInfo = { success: false, message: 'HTTP error' };
						updateImageInfo();
					},
				});
			}
		}
	}
	
	this.getActiveTool = function()
	{
		return activeTool;
	}
	
	this.onFilterChanged = function(tool)
	{
		if (!activeTool || tools[activeTool].getName() != tool.getName())
			return;
			
		var active = views.filters.find('.'+mycss('filter')+'.ui-state-highlight');
		if (active.length != 1)
			return;
			
		var index = filters.length-1-views.filters.find('.'+mycss('filter')).index(active);
		
		var filter = filters[index];
		
		if ('tool' in filter && tools[activeTool].getName() == filter.tool.getName())
			updateCurrentFilter();
	}
	
	this.pubLayout = function()
	{
		layout();
	}
	
	this.getFilters = function()
	{
		// We must return only the data part of the filter, not the complete one with
		// its bindings - the client does not need to know that, and further, we do not
		// want those objects to persist after the editor object is destroyed.
		var ret = [];
		for (var i in filters)
		{
			var filter = $.extend({}, filters[i]);
			delete filter.tool;
			delete filter.label;
			delete filter.dom;
			
			ret.push(filter);
		}
		return ret;
	}
	
	// ------------------------------
	// Private variables
	// ------------------------------
	
	var self = this;
	var views = {};
	var splitters = {};
	var filters = [];
	var tools = {};
	var toolsToInstall = null;
	var activeTool = null;
	var imageDirty = 1;
	var imageLoading = true;
	var imageInfo = null;
	
	// ------------------------------
	// Private functions
	// ------------------------------
	
	var divid = function(suffix)
	{
		var ret = '';
		if (params.idPrefix) ret += params.idPrefix;
		ret += suffix;
		return ret;
	}
	
	var mycss = function(suffix)
	{
		return params.cssPrefix + suffix;
	}

	var buildUI = function()
	{
		var mainDiv = $('#'+params.divid);
		
		mainDiv.addClass(mycss('main'));
		
		views.image = $('<div/>').addClass(mycss('image_view'));
		
		var img = $('<img />');
		img.attr('alt', 'image loading ...')
			.bind('load', function(e) { onImageLoad(true, e); } )
			.bind('error', function(e) { onImageLoad(false, e); })
			.attr('src', params.image.href);
		
		views.image.append(img);
		
		
		views.tools = $('<div />').addClass(mycss('tools_view'));
		var tabs = $('<div />').appendTo(views.tools);
		
		var ul = $('<ul>').appendTo(tabs);
		
		var addTab = function(id, title)
		{
			var a = $('<a />').attr('href', '#'+divid(id)).text(title).data(divid('data'), id);
			ul.append($('<li/>').append(a));
			var content = $('<div />').attr('id', divid(id)).data(divid('data'), id);
			tabs.append(content);
			return content;
		}
		
		var addTool = function(tabid, tool)
		{
			
			var p = allTabs[tabid];
			
			var icon = $('<div />').addClass('ui-icon');
			var label = $('<div />')
				.addClass('ui-label')
				.append($('<span />').text(tool.getLabel()))
	//			.text(tool.getLabel())
				.disableSelection()
			;
			
			var button = $('<div/>')
				.addClass(mycss('tool_button'))
				.append(icon)
				.append(label)
			;
			button.button();
			
			button.click(function() {
				self.setActiveTool(tool.getName());
			});
			
			p.append(button);
		}
		
		var allTabsList = [
			['common', 'Common'],
		];
		var allTabs = {};
		for (var i in allTabsList)
		{
			var t = addTab(allTabsList[i][0], allTabsList[i][1]);
			allTabs[allTabsList[i][0]] = t;
		}
			
		for (var i in toolsToInstall)
		{
			var tool = toolsToInstall[i][0];
			tool.init(self);
			tools[tool.getName()] = tool;
			
			for (var j in toolsToInstall[i][1])
			{
				addTool(toolsToInstall[i][1][j], tool);
			}
		}

		tabs.tabs();
		
		var toolPane = $('<div />')
			.addClass(mycss('tools_options'))
			.addClass('ui-widget ui-widget-content ui-corner-top')
			.appendTo(views.tools);

		var addfilter = $('<a href="#add" />').text('Add filter').button().click(function(e) {
			e.preventDefault();
			
			var tool = tools[activeTool];
			var filter = $.extend({type:tool.getName(),label:tool.getLabel(),tool:tool},tool.getFilter());
			addFilter(filter);
		}).css('visibility','hidden');
		
		// var updatefilter = $('<a href="#update" />').text('Update').button().click(function(e) {
			// e.preventDefault();
			// updateCurrentFilter();
		// }).css('visibility','hidden');
		
		/*var delfilter = $('<span />').text('Remove').button().click(function() {
			removeFilter(tools[activeTool].getFilter());
		});*/
		
		var toolActions = $('<div />')
			.addClass(mycss('tools_actions'))
			.addClass('ui-widget ui-widget-content ui-corner-bottom')
			//.text('Commands')
			.append(addfilter)
			// .append(updatefilter)
			//.append(delfilter)
			.appendTo(views.tools);
		
		views.filters = $('<div />').addClass(mycss('filters_view')+' ui-widget');
		views.filters.append($('<div class="ui-widget-header ui-corner-top" />').text('Filters'));
		views.filters.append($('<div class="ui-widget-content ui-corner-bottom" />').sortable({
			items: '>div:not(._original)',
		}));
		//views.filters.append('<div style="clear:both" />');
		
		mainDiv.append(views.image);
		mainDiv.append(views.tools);
		mainDiv.append(views.filters);
		
		toolsToInstall = null;
	}
	
	var layout = function()
	{
		//
		var mainDiv = $('#'+params.divid);
		
		var dims = [mainDiv.width(), mainDiv.height()];
		//
		// var docdims = [$(document).width(), $(document).height()];
		// var windims = [$(window).width(), $(window).height()];
		// 
		// 
		
		var imageDiv = mainDiv.find('.'+mycss('image_view'));

		var imageDivDims = [imageDiv.outerWidth(), imageDiv.height()];
		//
		//views.image.css('width', imageDivDims[0]+'px').css('height', imageDivDims[1]+'px');
		
		var imgProp = params.image.width/params.image.height;
		var viewProp = imageDivDims[0]/imageDivDims[1];
		var imageDims = [0,0];
		var imageOffset = [0,0];
		if (imgProp > viewProp)
		{
			imageDims[0] = imageDivDims[0];
			imageDims[1] = imageDivDims[0]/imgProp;
			imageOffset[1] = 0.5*(imageDivDims[1]-imageDims[1]);
		}
		else
		{
			imageDims[0] = imageDivDims[1]*imgProp;
			imageDims[1] = imageDivDims[1];
			imageOffset[0] = 0.5*(imageDivDims[0]-imageDims[0]);
		}
		views.image.find('img')
			.css('width', imageDims[0]+'px').css('height', imageDims[1]+'px')
			.css('left', imageOffset[0]+'px').css('top', imageOffset[1]+'px')
		;
		
		views.tools.css('left', imageDivDims[0]+'px');
		
		views.filters.css('left', imageDivDims[0]+'px').css('top', views.tools.outerHeight()+'px');
		
		var optionsH = views.tools.outerHeight() - views.tools.find('.'+mycss('tools_actions')).outerHeight() - views.tools.find('>div.ui-tabs').outerHeight();
		views.tools.find('.'+mycss('tools_options')).outerHeight(optionsH);
		
		var filterH = views.filters.outerHeight() - views.filters.find('div.ui-widget-header').outerHeight();
		views.filters.find('div.ui-widget-content').outerHeight(filterH);
		
	}
	
	var addFilter = function(filter)
	{
		if (!('active' in filter))
			filter.active = true;
			
		var item = $('<div />').addClass(mycss('filter'));
		
		var txt = $('<div />').text(filter.label);
		
		if (filter.type != 'original')
		{
			var vis = $('<div class="ui-icon '+(filter.active?'ui-icon-bullet':'ui-icon-radio-on')+' ui-inline-icon" />');
			vis.click(function() {
				var item = $(this).parent('.'+mycss('filter'));
				var domIndex = views.filters.find('.'+mycss('filter')).index(item);
				var arrIndex = filters.length-1-domIndex;
				
				var filter = filters[arrIndex];
				if (filter.active)
				{
					$(this).removeClass('ui-icon-bullet').addClass('ui-icon-radio-on');
					filter.active = false;
				}
				else
				{
					$(this).removeClass('ui-icon-radio-on').addClass('ui-icon-bullet');
					filter.active = true;
				}
				dirty();
			});
			
			var del = $('<div class="ui-icon ui-icon-closethick ui-inline-icon" />')
			del.click(function() {
				var item = $(this).parent('.'+mycss('filter'));
				var domIndex = views.filters.find('.'+mycss('filter')).index(item);
				var arrIndex = filters.length-1-domIndex;
				
				var wasSelected = item.hasClass('ui-state-highlight');
				item.remove();
				filters.splice(arrIndex,1);
				dirty();
				if (wasSelected)
				{
					var newSel = Math.max(0, arrIndex-1);
					selectFilterByIndex(newSel);
				}
			});
			
			item.append(vis);
			item.append(del);
		}
		item.append(txt);
		
		item.data(divid('data', filter));
		filter.dom = item;
		
		filters.push(filter);

		if (filter.type == 'original')
		{
			item.addClass('_original');
		}
		else
		{
			dirty();
		}
		
		views.filters.find('div.ui-widget-content').prepend(item);

		selectFilterByIndex(filters.length-1);
		
		item.bind('click', function() {
			selectFilterByIndex(filters.length-1-views.filters.find('.'+mycss('filter')).index(this));
		});
	}
	
	var dirty = function()
	{
		//
		imageDirty++;
		if (!imageLoading)
			sync();
	}
	
	var sync = function()
	{
		

		var url = params.serverScript+'?'+ImageEditor.buildQueryString(filters);
		
		//

		// Just to see the request
		/*$.ajax({
			type: 'get',
			url: url,
		});*/
		
		var img = $('#'+params.divid+' .'+mycss('image_view')+' img');
		
		
		imageLoading = true;
		img.attr('src', url);
	}
	
	var onImageLoad = function(success, event)
	{
		
		//
		imageLoading = false;
		imageDirty--;
		
		if (imageDirty)
			sync();
	}
	
	var selectFilterByIndex = function(index)
	{
		
		var filter = filters[index];
		
		var dompos = filters.length-1-index;

		// Highlight the selected filter
		views.filters.find('.'+mycss('filter'))
			.removeClass('ui-state-highlight')
			.addClass('ui-state-default')
			.eq(dompos)
				.addClass('ui-state-highlight');
		
		if ('tool' in filter)
		{
			// Show the options panel for the corresponding tool,
			// showing the correct tab at the same time.
			self.setActiveTool(filter.tool.getName(), true);
		
			// Update the panel's values to match the filter's
			filter.tool.setFilter(filter);
		}
		else
			self.setActiveTool(null);
	}
	
	var updateCurrentFilter = function()
	{
		
		var tool = tools[activeTool];

		var newfilter = $.extend({type:tool.getName(),label:tool.getLabel(),tool:tool,active:true},tool.getFilter());
		var active = views.filters.find('.'+mycss('filter')+'.ui-state-highlight');
		

		var index = filters.length-1-views.filters.find('.'+mycss('filter')).index(active);
		

		var oldfilter = filters[index];
		

		delete filters[index];
		filters[index] = newfilter;
		
		dirty();
	}
	
}

ImageEditor.resizeEventHandler = null;

ImageEditor.buildQueryString = function(filters)
{
	var data = [];
	for (var i in filters)
	{
		var f = $.extend({}, filters[i]);
		if (!f.active)
			continue;
		delete f.label;
		delete f.tool;
		delete f.active;
		delete f.dom;
		data.push(f);
	}
	var serialized = JSON.stringify(data);
	var qs = 'action=get&filters='+encodeURIComponent(serialized);
	
	return qs;
}
	


/* imageeditor_brightness_contrast.js */
/*
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 ImageEditor_BrightnessContrast(params)
{
	// ------------------------------
	// Constructor
	// ------------------------------
	
	params = params || {};
	if (!params) params = new Object();
	if (!params.cssPrefix) params.cssPrefix = 'imageeditor_';
	
	// ------------------------------
	// Public methods
	// ------------------------------
	
	this.init = function(editorObj)
	{
		editor = editorObj;
	}

	this.getPanel = function()
	{
		if (!panel)
		{
			buildUI();
		}
		return panel;
	}
	
	this.getName = function()
	{
		return 'brightness_contrast';
	}
	
	this.getLabel = function()
	{
		return 'Brightness / Contrast';
	}
	
	this.getHelp = function()
	{
		return [
			'Adjust the brightness and/or contrast of the image.',
			'Brightness and Contrast values apply changes to the input image. They are not absolute settings. A brightness or contrast value of zero means no change. The range of values is -100 to +100 on each. Positive values increase the brightness or contrast and negative values decrease the brightness or contrast. To control only contrast, set the brightness to 0. To control only brightness, set the contrast to 0.'
		];
	}
	
	this.getFilter = function()
	{
		return {
			brightness: brightness,
			contrast: contrast,
		};
	}
	
	this.setFilter = function(filter)
	{
		panel.find('._brightness .ui-slider').slider('value', filter.brightness);
		panel.find('._brightness ._value').text(filter.brightness);
		
		panel.find('._contrast .ui-slider').slider('value', filter.contrast);
		panel.find('._contrast ._value').text(filter.contrast);
	}
	
	// ------------------------------
	// Private variables
	// ------------------------------
	
	var self = this;
	var panel = null;
	var editor = null;
	
	var brightness = 0;
	var contrast = 0;
	
	// ------------------------------
	// Private functions
	// ------------------------------

	var mycss = function(suffix)
	{
		return params.cssPrefix + suffix;
	}

	var buildUI = function()
	{
		
		
		panel = $('<div />').addClass(mycss('tool_panel'));
		
		panel.append(createToolSlider('Brightness', { 
			outerClass: mycss('tool_slider')+' _brightness',
			min: -100,
			max: 100,
			value: 0,
			onSlide: function(value) {
				brightness = value;
			},
			stop: function(event,ui) {
				//
				editor.onFilterChanged(self);
			},
		}));

		panel.append(createToolSlider('Contrast', { 
			outerClass: mycss('tool_slider')+' _contrast',
			min: -100,
			max: 100,
			value: 0,
			onSlide: function(value) {
				contrast = value;
			},
			stop: function(event,ui) {
				//
				editor.onFilterChanged(self);
			},
		}));
	}
}


/* imageeditor_invert.js */
/*
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 ImageEditor_Invert(params)
{
	// ------------------------------
	// Constructor
	// ------------------------------
	
	params = params || {};
	if (!params) params = new Object();
	if (!params.cssPrefix) params.cssPrefix = 'imageeditor_';
	
	// ------------------------------
	// Public methods
	// ------------------------------
	
	this.init = function(editorObj)
	{
		editor = editorObj;
	}

	this.getPanel = function()
	{
		if (!panel)
		{
			buildUI();
		}
		return panel;
	}
	
	this.getName = function()
	{
		return 'invert';
	}
	
	this.getLabel = function()
	{
		return 'Invert';
	}
	
	this.getHelp = function()
	{
		return [
			'Replace each pixel with its complementary color.',
			'The red, green, and blue intensities of an image are negated. White becomes black, yellow becomes blue, etc.'
		];
	}
	
	this.getFilter = function()
	{
		return {
			grayscale: grayscale,
		};
	}
	
	this.setFilter = function(filter)
	{
	}
	
	// ------------------------------
	// Private variables
	// ------------------------------
	
	var self = this;
	var panel = null;
	var editor = null;
	
	var grayscale = false;
	
	// ------------------------------
	// Private functions
	// ------------------------------

	var mycss = function(suffix)
	{
		return params.cssPrefix + suffix;
	}

	var buildUI = function()
	{
		
		
		panel = $('<div />').addClass(mycss('tool_panel'));
		
		var randomID = 'imageeditor_tool_invert'+(new Date().getTime());
		var grayscaleBox = $('<input type="checkbox" '+(grayscale?'checked="checked"':'')+' id="'+randomID+'" />');
		grayscaleBox.bind('click', function() { 
			grayscale = this.checked; 
			editor.onFilterChanged(self);
		});
		var label = $('<label for="'+randomID+'" />').text('Apply to grayscale pixels only');
		
		panel.append(grayscaleBox).append(label);
	}
}


/* pianos4.js */
/*
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/.
*/

/**
 * The main object representing the interface (*needs* to be a singleton)
 * @constructor
 */
function Pianos4(params)
{
	// ------------------------------
	// Constructor
	// ------------------------------
	params = params || {};
	if (!params) params = new Object();
	if (!params.version) params.version = '4 (unknown release number)';
	if (!params.divid) params.divid = 'pianos';
	if (!params.title) params.title = null;
	
	if (!params.phase) params.phase = 'analysis';
	if (!params.phases) params.phases = {'analysis':true,'comparison':true,'evaluation':true};
	if (!params.mark) params.mark = null;
	if (!params.print) params.print = null;
	if (!params.minutiaTypes) params.minutiaeTypes = {1:'bifurcation', 2:'ridge_ending', 3:'center', 4:'unknown' };
	params.minutiaeTypesI = {};
	for (var x in params.minutiaeTypes)
		params.minutiaeTypesI[params.minutiaeTypes[x]] = x;
	if (!params.loadResults) params.loadResults = null;
	if (!params.loadResultsLabel) params.loadResultsLabel = 'Load';
	if (!params.saveResults) params.saveResults = null;
	if (!params.submitResults) params.submitResults = null;
	if (!params.zoomFactorIncrement) params.zoomFactorIncrement = 2;
	if (!params.screenResolution) params.screenResolution = 72.0;
	if (!params.cssPath) params.cssPath = 'css/';
	if (!params.v3) params.v3 = false;
	if (!params.imageEditorScript) params.imageEditorScript = null;
	if (!params.matcherScript) params.matcherScript = null;
	if (!params.intersectScript) params.intersectScript = null;
	if (!params.debug) params.debug = false;
	if (!params.user) params.user = { id: 0, name: 'Unnamed user', tutor: false };
	if (typeof(params.hotSwitchPhase) == 'undefined') params.hotSwitchPhase = false; 	// This, in theory, enables one to activate the phase directly, without reload. In the current state, this doesn't work as the UI is created based on the said phase. For it to work, the application must be re-designed so that the whole UI is always created, and its element are shown/hidden based on the mode. The non-hotSwitchPhase is nevertheless still better than in PiAnoS 3 as the results are cached - the "reload" is just about deleting the pianos object and recreating it. No data will be reloaded from the server.
	if (!params.initData) params.initData = null;
	if (!params.linkToPhases) params.linkToPhases = {};
	if (typeof(params.minutiaToolAutoPairing) == 'undefined') params.minutiaToolAutoPairing = false; 
	if (!params.layerOnOffHover) params.layerOnOffHover = ['ui-state-hover','ui-state-disabled','ui-state-active'];
	if (typeof(params.useAreaTool) == 'undefined') params.useAreaTool = true; 
	if (typeof(params.useCommentTool) == 'undefined') params.useCommentTool = true; 
	if (typeof(params.useQualityTool) == 'undefined') params.useQualityTool = false; 
	if (typeof(params.useWCSTool) == 'undefined') params.useWCSTool = false; 
	if (typeof(params.useArcTool) == 'undefined') params.useArcTool = false; 
	if (typeof(params.useGroupTool) == 'undefined') params.useGroupTool = false; 
	if (!params.editToolHotkey) params.editToolHotkey = 'ctrl';
	if (!params.layerNames) params.layerNames = {};
	if (!params.layoutBorderWidth) params.layoutBorderWidth = 2;
	if (!params.checkedSubmit) params.checkedSubmit = params.v3;
	if (typeof(params.hasClearButton) == 'undefined') params.hasClearButton = true;
	if (typeof(params.hasLoadButton) == 'undefined') params.hasLoadButton = true;
	if (typeof(params.inferEditTool) == 'undefined') params.inferEditTool = false;
	if (!params.initialTool) params.initialTool = 'edit';
	if (typeof(params.startsFullscreen) == 'undefined') params.startsFullscreen = false;
	if (typeof(params.showModifiedMinutiae) == 'undefined') params.showModifiedMinutiae = false;
	if (!params.updatedMinutiaeTimestamp) params.updatedMinutiaeTimestamp = null;
	if (typeof(params.showTimestampsInProperties) == 'undefined') params.showTimestampsInProperties = false;
	if (typeof(params.useBrushTool) == 'undefined') params.useBrushTool = false;
	if (typeof(params.defaultSuitabilityApproach) != 'number') params.defaultSuitabilityApproach = null;
	if (typeof(params.showPreferences) == 'undefined') params.showPreferences = false;
	if (!params.preferencesScript) params.preferencesScript = null;
	if (!params.onExerciseStartup) params.onExerciseStartup = null;
	if (!params.userManualURL) params.userManualURL = null;
	if (!params.reportToolURL) params.reportToolURL = null;
	if (!params.getConclusionsScript) params.getConclusionsScript = null;
	if (!params.resultsFilterScript) params.resultsFilterScript = null;
	if (typeof(params.numResultsHideThreshold) == 'undefined') params.numResultsHideThreshold = 50;
	if (typeof(params.useHotkeys) == 'undefined') params.useHotkeys = true;
	if (typeof(params.showToolbox) == 'undefined') params.showToolbox = true;
	if (typeof params.markCanHaveCenter == 'undefined') params.markCanHaveCenter = false;
	if (typeof params.startsWithConclusionOpen == 'undefined') params.startsWithConclusionOpen = false;
	
	// This function fiddles with the parameters
	(function() {
		var defaultLayerNames = {
			'images': 'Images',
			'areas': 'Areas',
			'others': 'Other features',
			'ridges': 'Ridges',
			'minutiae': 'Minutiae',
			'pairing': 'Pairing',
			'segments': 'Segments',
			'comments': 'Comments',
		};
		for (var i in defaultLayerNames) {
			if (!(i in params.layerNames)) params.layerNames[i] = defaultLayerNames[i];
		}
		
		var preProcessedScripts = ['imageEditorScript','matcherScript','intersectScript'];
		for (var i in preProcessedScripts)
		{
			var script = preProcessedScripts[i];
			if (params[script])
			{
				var scr = params[script];
				scr = scr.replace(new RegExp('%%protocol%%'), window.location.protocol);
				scr = scr.replace(new RegExp('%%hostname%%'), window.location.hostname);
				params[script] = scr;
			}			
		}
		
		if (params.userManualURL)
		{
			var mainHelpDef = Pianos4Help.topics.general[1];
			mainHelpDef.splice(1,0,'[[User manual|^'+params.userManualURL+']]');
		}
	})();

	// ------------------------------
	// Public methods
	// ------------------------------
	
	this.init = function()
	{
		var activeTools = [
			new Pianos4ToolEdit({
				v3:params.v3,
				hotkey: params.editToolHotkey,
			}),
		];
		
		if (!params.v3)
		{
			if (params.useQualityTool)
			{
				activeTools.push(new Pianos4ToolRidge({
					type:'quality',
					intersectScript:params.intersectScript,
					qualityDefaultAnsiNist: false,
					qualityTypeSelector: 'dropdown',
				}));
			}
		}
		
		activeTools.push(new Pianos4ToolMinutia({
			v3:params.v3,
			autoPairing:params.minutiaToolAutoPairing,
			phase:params.phase,
			singleClickDeselects: params.inferEditTool,
			markCanHaveCenter: params.markCanHaveCenter,
		}));
		activeTools.push(new Pianos4ToolRidge());
		
		if (!params.v3)
		{
			if (params.useAreaTool)
				activeTools.push(new Pianos4ToolRidge({type:'area'}));
			if (params.useBrushTool)
				activeTools.push(new Pianos4ToolBrush());
			if (params.useCommentTool)
				activeTools.push(new Pianos4ToolComment());
			if (params.useArcTool)
				activeTools.push(new Pianos4ToolArc());
			if (params.useWCSTool)
				activeTools.push(new Pianos4ToolRidge({type:'wcs'}));
			if (params.useGroupTool)
				activeTools.push(new Pianos4ToolGroup());
			if (params.phase != 'analysis')
				activeTools.push(new Pianos4ToolPairing());	
		}

		activeTools.push(new Pianos4ToolPan({phase:params.phase}));

		if (params.user.tutor)
		{
			activeTools.push(new Pianos4ToolReview());
		}
		
		for (var tool in activeTools)
		{
			var tool = activeTools[tool];
			tools[tool.getName()] = tool;
		}
		
		activeTool = params.initialTool;
		buildUI();
		
		if (typeof(params.screenResolution) == 'object')
		{
			screenResolution = calculateScreenResolution(params.screenResolution);
		}
		else if (typeof(params.screenResolution) != 'number')
		{
			screenResolution = parseFloat(params.screenResolution);
		}
		else
		{
			screenResolution = params.screenResolution;
		}
		
		self.setZoomFit();
		
		if (params.useHotkeys)
			registerHotkeys();
		/*self.registerHandler('contextmenu', function(e) {
			if (skipNextContextMenu) 
			{
				skipNextContextMenu = false;
				e.preventDefault(); 
				return false;
			}
		});*/
		
		if (params.initData)
		{
			onDataLoaded(params.initData);
			updateUI();
		}
		else if (params.loadResults)
		{
			// load() will call ajax() which will in turn call updateUI()
			self.load();
		}
		else
		{
			onConclusionUpdated(true); // This will also be called after load()
			updateUI();
		}
		
		activeTool = null;
		setActiveTool(params.initialTool);
		
		if (params.startsWithConclusionOpen)
			toggleConclusion();
		
		if (params.onExerciseStartup)
			params.onExerciseStartup(self, params);
	}

	// udb.app compat layer
	this.run = function()
	{
		self.has_updateUI = true;
		self.updateUI = function() {};
		self.init();
	}

	this.getVersion = function()
	{
		return params.version;
	}
	
	this.load = function(useAction)
	{
		
		
		self.ajax({
			pendingMessage: 'Loading',
			url: params.loadResults,
			success: function(data) {
				if (('status' in data && data.status == 'ok') || ('error' in data && !data.error))
				{
					onDataLoaded(data.ret, useAction);
				}
				else
				{
					alert('Error: could not load the results\n\n'+data.message);
				}
			},
		});
	}
	
	this.getServerData = function()
	{
		return serverData;
	}
	
	this.save = function(submit, force)
	{
		
		
		if (submit && !force && params.checkedSubmit)
		{
			var message = null;
			var message2 = null;
			var links = [];
			var focus = null;
			var yes = null;
			var title = 'Confirm submit ?';
			
			var overrideMessage = 'Ignore this message and submit the results anyway (no way back !)';
			var overrideAction = function() { 
				self.save(submit, true); 
			};
				
			var conclusions = getConclusions();
			var closeButtonLabel = 'Close';
			
			if (!conclusions.mandatoryOK)
			{
				var fields = getConclusionFields();
				// This will map unanswered questions to their numbered labels to help the users answer
				// the appropriate question(s). This mapping should make it into the 'fields' variable
				// above but this implies refactoring, which I have no time for.
				// TODO: merge the numbers to the fields, make the script apply numbers to the questions
				// (they are applied in buildStatusBar() for the moment) and get rid of 'uglyMapping'
				var uglyMapping;
				if (params.phase == 'analysis')
				{
					uglyMapping = {
						'mark-degradation': '1',
						'mark-gp': '2',
						'mark-details': '3',
						'mark-suitability-approach': '4b',
						'mark-value': '4',
					};
				}
				else if (params.phase == 'comparison')
				{
					uglyMapping = {
						'comparison-decision': '1',
						'comparison-decision-ex': '1b',
						'comparison-need-better-known': '1c',
						'comparison-l3details': '2',
						'comparison-qualquant': '3',
					};
				}
				var missing = [];
				for (var i in fields)
				{
					var field = fields[i];
					if (field[2] in uglyMapping)
					{
						if (!(field[2] in conclusions) && field[1].is(':visible'))
						{
							missing.push(uglyMapping[field[2]]);
							if (!focus)
								focus = field[1].parent('div');
							else
								focus = focus.add(field[1].parent('div'));
						}
					}
				}
				
				missing.sort();
				if (missing.length == 1)
				{
					message = 'A question regarding your conclusions is unanswered. You must answer it to proceed.';
					message2 = 'Missing item: '+missing[0]+'.';
				}
				else
				{
					message = 'Some questions regarding your conclusions are unanswered. You must answer all of them to proceed.';
					var last = missing.pop();
					message2 = 'Missing items: '+missing.join(', ')+' and '+last+'.';
				}
				title = 'Not all questions were answered';
			}
			else
			{
				var as = self.getAnnotationsSummary();
				
				if (as.mark && as.mark.count == 0)
					message = 'You must annotate the quality and minutiae on the mark before you can proceed.';
				else if (as.mark && as.mark.num_areas == 0)
					message = 'You must annotate the quality areas on the mark before you can proceed.';
				else if (as.mark && as.mark.num_minutiae == 0)
					message = 'You must annotate the minutiae on the mark before you can proceed.';
				else if (as.print && as.print.num_minutiae == 0)
					message = 'You must annotate the minutiae on the print before you can proceed.';
				else if (as.print && as.num_paired_minutiae == 0)
				{
					message = 'We have noticed that you are submitting your results but have not paired any minutiae.  We ask that all corresponding minutiae be paired together.  Is this an oversight?  Do you wish to continue and submit your results or go back and pair minutiae?';
					closeButtonLabel = 'Return to pair minutiae';
					overrideMessage = 'Continue and submit results with no paired minutiae (no way back !)';
					links.push([overrideMessage, overrideAction]);
				}
				
				if (message)
					title = 'Missing annotations';
			}
			
			if (!message && !conclusions.isOK)
			{
				title = 'No case notes';
				message = 'At this stage you have not documented your observations with case notes. Feel free to take more case notes as you see appropriate.';
				links.push([overrideMessage, overrideAction]);
				focus = $('.pianos4_observations textarea');
				closeButtonLabel = 'Back to case notes';
			}
			
			if (!message)
			{
				message = 'Do you really want to submit your results and proceed to the next step ? There is no way back.';
				yes = overrideAction;
			}
			
			if (message)
			{
				var dlg = $('<div class="pianos4_dialog" />')
					.append($('<div class="pianos4_dialog_main" />').text(message));
				
				if (message2)
					dlg.append($('<div class="pianos4_dialog_main" />').css('margin-top','1em').text(message2));
					
				if (links) for (var i in links) 
				{
					var link = links[i];
					dlg.append($('<div class="pianos4_dialog_links" />').append($('<span  />').text(link[0]).bind('click',function(e) {
						e.preventDefault();
						link[1]();
					})));
				}
				
				if (focus)
				{
					if (!$('.ui-layout-south .pianos4_conclusions a').hasClass('_open'))
						toggleConclusion();
					focus.addClass('ui-state-error');
					focus.effect('pulsate');
				}
					
				if (yes)
				{
					buttons = {
						'Yes': function() { 
							$(this).dialog('close');
							yes();
						},
						'No': function() {
							$(this).dialog('close');
						},
					};
				}
				else
				{
					buttons = {};
					buttons[closeButtonLabel] = function() { 
							$(this).dialog('close');
					};
				}
					
				self.dialog(dlg, {
					autoOpen: true,
					title: title,
					buttons: buttons,
					modal: true,
					width: 400,
					close: function() {
						if (focus)
						{
							focus.removeClass('ui-state-error', 1000);
							focus.focus();
						}
						dlg.remove();
					},
				});

				return;
			}
		}
		
		
		
		self.ajax({
			pendingMessage: 'Saving',
			type: 'post',
			processData: false,
			contentType: 'application/json',
			url: submit ? params.submitResults : params.saveResults,
			data: JSON.stringify(params.v3 ? self.getDataV3(submit) : self.getData()),
			success: function(data) {
				if (('status' in data && data.status != 'ok') || ('error' in data && data.error))
					alert('Error: could not save the results\n\n'+data.message);
				else
				{
					lastSaveData.stamp = new Date().getTime();
					lastSaveData.stackPtr = stackPtr;
					if (submit)
						window.location.reload();
				}
			},
		});
	}

	this.ajax = function(options)
	{
		var o = {
			type: 'get',
			dataType: 'json',
		};
		$.extend(o, options);
		
		o.success = function(data, status, xhr)
		{
			
			delete operations[xhr.pianos4_id];
			--operations.length;

			if (options.success)
				options.success(data, status, xhr)

			updateUI();
		}
		
		o.error = function(xhr, status, error)
		{
			
			delete operations[xhr.pianos4_id];
			--operations.length;

			if (options.error)
				options.error(xhr, status, error)

			updateUI();
		}
		
		o.beforeSend = function(xhr, settings)
		{
			
			xhr.pianos4_id = xhrID++;
			operations[xhr.pianos4_id] = 'pendingMessage' in options ? options.pendingMessage : 'Working ...';
			++operations.length;
			updateUI();
		}
		
		$.ajax(o);
	}

	this.enterModal = function()
	{
		
		doingModal++;
		
		updateUI();
	}
	
	this.leaveModal = function()
	{
		
		doingModal--;
		
		updateUI();
	}
	
	this.dialog = function(element, options)
	{
		var o = {
			// Insert default dialog options here if needed
 		};
		$.extend(o, options);
		
		var onOpen = function(event, ui)
		{
			self.enterModal();
		}

		var onClose = function(event, ui)
		{
			self.leaveModal();
		}
		
		element
			.bind('dialogopen', onOpen)
			.bind('dialogclose', onClose)
			.dialog(options);
	}
	
	this.getActiveTool = function()
	{
		if (!activeTool)
			return null;
		
		return tools[activeTool];
	}
	
	this.getTool = function(name)
	{
		
		return tools[name];
	}

	this.getView = function(name)
	{
		
		if (name == 'mark') {  return mark; }
		else if (name == 'print') {  return print; }
	}
	
	this.getViews = function()
	{
		var r = [];
		if (mark)
			r.push(mark);
		if (print)
			r.push(print);
		return r;
	}
	
	this.exec = function(action)
	{
		action.redo();
		
		// Cut the stack where the pointer currently is - this will loose
		// track of all modifications that were rolled back to that point
		// (in other words, the chain of actions available for "redo" is
		// destroyed).
		if (stack.length > 0 && stackPtr < stack.length-1)
		{
			if (lastSaveData.stackPtr > stackPtr)
				// Clear the lastSaveData's stack ptr since it points to an area
				// of the stack that has been invalidated.
				lastSaveData.stackPtr = -1
			stack = stack.slice(0, stackPtr+1);
		}
		stack.push(action);
		++stackPtr;
		
		updateUI();
	}
	
	this.undo = function(noUpdateUI)
	{
		
		var action = stack[stackPtr];
		action.undo();
		--stackPtr;
		
		if (!noUpdateUI)
			updateUI();
	}
	
	this.redo = function(noUpdateUI)
	{
		
		++stackPtr;
		var action = stack[stackPtr];
		action.redo();
		
		if (!noUpdateUI)
			updateUI();
	}
	
	this.getNewZoomFactor = function(factor, out)
	{
		var sliderval = getSliderValueFromZoomFactor(factor);
		sliderval += out ? -params.zoomFactorIncrement : params.zoomFactorIncrement;
		var slider = $('#'+params.divid+' .pianos4_zoombox_slider');
		sliderval = Math.min(Math.max(sliderval, slider.slider('option','min')), slider.slider('option','max'));
		return getZoomFactorFromSliderValue(sliderval);
	}
	
	this.setOtherViewZoomFactor = function(view, factor, anchorOffset)
	{
		var target = view == mark ? print : mark;
		if (target)
			target.setZoomFactor(factor, null, false, anchorOffset);
		updateZoomSlider(factor);
	}
	
	
	this.setDataV3 = function(data)
	{
		Pianos4Annotation._s_ids = 1;
		
		if (mark) mark.clear();
		if (print) print.clear();
		
		if (params.user.tutor)
			panes.results.find('table>tbody').html('');
			
		viewWithFocus = null;
		self.onSelectionChanged(null);
		
		users = {};
		
		for (var i in data)
		{
			var assignment = data[i];
			
			users[assignment.userid] = {
				id: assignment.userid,
				name: assignment.username,
				lr: null
			};
				
			for (var restype in assignment.results)
			{
				var result = assignment.results[restype];
				
				var view = null;
				if (restype == 'analysis_trace' && displayedPhase == 'analysis')
					view = mark;
				else if (restype == 'comparison_trace' && (displayedPhase == 'comparison' || displayedPhase == 'evaluation'))
					view = mark;
				else if (restype == 'comparison_print' && (displayedPhase == 'comparison' || displayedPhase == 'evaluation'))
					view = print;
					
				if (!view)
					continue;
					
				if ('minutiae' in result)
				{
					for (var j in result.minutiae)
					{
						var m = result.minutiae[j];
						
						var mobj = new Pianos4Minutia(params.minutiaeTypes[m.type], [m.x,m.y], clampAngle(m.angle));
						mobj.userid = assignment.userid;
						view.addMinutia(mobj);
					}
				}
				if ('ridges' in result)
				{
					for (var j in result.ridges)
					{
						var r = result.ridges[j];
						var robj = new Pianos4Ridge(r.points, r.color, r.width, r.alpha/100.0);
						robj.userid = assignment.userid;
						view.addRidge(robj);
					}
				}
				
				if (displayedPhase == 'analysis' && restype == 'analysis_trace')
					$('.pianos4_observations textarea').val(result.conclusion);
				else if (displayedPhase == 'comparison' && restype == 'comparison_print')
					$('.pianos4_observations textarea').val(result.conclusion);
			}
		}

		if (params.user.tutor)
			updateResultsPanel();
	}
	
	this.getDataV3 = function(submit)
	{
		var data = self.getData();
		data['general'] = {
			'phase': params.phase,
			'validated': submit,
			'conclusion': $('.pianos4_observations textarea').val(),
		};
		return data;
	}

	this.getData = function(annotationTypeFilter, filterCallback)
	{
		var data = {};

		$(allViews).each(function() {
			data[this.getType()] = this.getData(annotationTypeFilter, filterCallback);
		});
		
		if (!params.user.tutor)
		{
			data.observations = getConclusions();
			delete data.observations.length; // 'length' is set by getConclusions() but isn't actual data
			delete data.observations.isOK; // Same remark
			delete data.observations.mandatoryOK;
		}
		
		return data;
	}
	
	this.setData = function(data)
	{
		Pianos4Annotation._s_ids = 1;
		
		if (mark) mark.clear();
		if (print) print.clear();
		
		if (params.user.tutor)
			panes.results.find('table>tbody').html('');
			
		viewWithFocus = null;
		self.onSelectionChanged(null);
		
		users = {};
		
		var dataVisible = true;
		
		if (data)
		{
			if (params.user.tutor && $.isArray(data))
			{
				var dataShown = null;
				if (filters !== null)
				{
					dataShown = [];
					for (var i in data)
					{
						var assignment = data[i];
						if (filterAssignment(assignment))
							dataShown.push(assignment);
					}
				}
				else
				{
					dataShown = data;
				}
				
				
				dataVisible = params.numResultsHideThreshold < 0 ? true : dataShown.length < params.numResultsHideThreshold;
				if (mark) mark.setNewAnnotationsHidden(!dataVisible);
				if (print) print.setNewAnnotationsHidden(!dataVisible);
				
				for (var i in dataShown)
				{
					var assignment = dataShown[i];
					
					
					var userobj = {
						id: assignment.user.id,
						name: assignment.user.name,
						lr: null,
						progress: assignment.progress,
					};
					if ('analysis_submitted_on' in userobj.progress)
						userobj.progress.analysis_submitted_on = pgTimestamptzToDate(userobj.progress.analysis_submitted_on);
					users[assignment.user.id] = userobj;
					
					if ('mark' in assignment && mark)
						mark.setData(assignment.mark, true);
					if ('print' in assignment && print)
						print.setData(assignment.print, true);
				}
				
				if (mark) mark.setNewAnnotationsHidden(false);
				if (print) print.setNewAnnotationsHidden(false);
			}
			else
			{
				if ('mark' in data && mark)
					mark.setData(data.mark);
				if ('print' in data && print)
					print.setData(data.print);
			}
		}
		
		if (params.user.tutor)
		{
			updateResultsPanel(!dataVisible);
			onTutorDataAvailable('results', data);
		}
		else if (data)
		{
			if ('observations' in data)
				self.setConclusions(data.observations);
			else
				onConclusionUpdated(true);
		}
		
	}
	
	this.setConclusions = function(data)
	{
		var fields = getConclusionFields();
		
		for (var i in fields)
		{
			var field = fields[i];
			switch (field[0])
			{
			case 'string':
				var valueToSet = field[2] in data ? data[field[2]] : '';
				field[1].val(valueToSet);
				
				break;
			case 'dropdown':
				var valueToSet = field[2] in data ? data[field[2]] : -1;
				field[1].val(valueToSet);
				// The call above can actually fail if the dropdown does not contain the key
				// that was passed in, so the assert() below makes sense.
				
				break;
			case 'data':
				if (field[2] in data)
					field[1].data('pianos4', data[field[2]]);
				else
					field[1].removeData('pianos4');
				break;
			default:
				
				break;
			}
		}
		
		onConclusionUpdated(true);
	}

	this.toggleLayer = function(layer, force)
	{
		var shown = [];
		if (mark) shown.push(mark.toggleLayer(layer, force));
		if (print) shown.push(print.toggleLayer(layer, force));
		
		var button = panes.layers.find('.button._layer_'+layer);
		
		var on = (shown.length >= 1 && shown[0]) || (shown.length >= 2 && shown[1]);
		
		if (on) 
		{
			button.removeClass(params.layerOnOffHover[1]).addClass(params.layerOnOffHover[0]);
			button.find('span').removeClass('ui-icon-closethick').addClass('ui-icon-check');
		} 
		else 
		{
			button.removeClass(params.layerOnOffHover[0]).addClass(params.layerOnOffHover[1]);
			button.find('span').removeClass('ui-icon-check').addClass('ui-icon-closethick');
		}
	}
	
	// Call when the selection has changed to update the properties panel. Pass-in the view
	// on which the latest event occurred. If null is given, clears the focus out of both
	// views and display 'No properties to show', as if nothing was selected in the current
	// view. Pass a boolean true value to re-use the current view with focus
	this.onSelectionChanged = function(view)
	{
		if (typeof(view) == 'boolean' && view)
			view = viewWithFocus;
			
		viewWithFocus = view;
		var numSel = 0;
		var headerText = '';
		var table = null;
		
		$('.pianos4_observations textarea').blur();
		
		if (view)
		{
			var sel = view.getSelection();
			//
			//
			
			var firstType = null;
			var multiTypes = false;
			
			table = $('<table />');
			var body = table.append($('<tbody />'));
			var counter = 0;
			for (var aid in sel) (function(annotationAndType)
			{
				
				var type = annotationAndType[0];
				var annotation = annotationAndType[1];
				
				if (firstType == null)
					firstType = type;
				else if (firstType != type)
					multiTypes = true;
				
				var kvs = [];
				
				if (numSel++ > 0)
					kvs.push('--');
					
				kvs.push(['id', type+' '+('oldid' in annotation ? annotation.oldid+' ('+annotation.id+')' : annotation.id)]);
				if (type == 'minutia')
				{
					kvs.push(['type', annotation.type]);
					kvs.push(['x', annotation.pos[0].toFixed(2)]);
					kvs.push(['y', annotation.pos[1].toFixed(2)]);
					kvs.push(['angle', annotation.angle.toFixed(2)]);
					if ('pairing' in annotation)
						kvs.push(['pairing', annotation.pairing]);
				}
				else if (type == 'ridge' || type == 'area')
				{
					kvs.push(['color', annotation.color, 'colorpick']);
					if (type == 'area' && 'type' in annotation)
					{	
						var scale = null;
						if (annotation.type == 'wcs')
						{
							kvs[0][1] = 'feature '+annotation.id;
							scale = Pianos4Constants.details;
						}
						else if (annotation.type == 'quality')
						{
							
							scale = Pianos4Constants.quality[tools['quality'].isQualityAnsiNist() ? 'ansi_nist' : 'standard'];
						}
						
						if (scale)
						{
							try {
								var item = Pianos4Constants.getItemById(scale, annotation.value);
								kvs.push(['type', item.label]);
							} catch (e) {
								// May fail if the quality ID was set to e.g. 5 in ansi_nist mode, then the mode was changed to standard
								// Don't print anything in this case - not too much harm anyway
								// The actual fix will be an automated translation of ALL quality areas when the mode is changed.
							}
						}
					}
				}
				
				if (params.showTimestampsInProperties)
				{
					kvs.push(['created', annotation.created.toISOString()]);
					kvs.push(['modified', annotation.modified.toISOString()]);
				}
				
				if (params.user.tutor)
				{
					
					if (annotation.userid == params.user.id)
						kvs.push(['user', params.user.name]);
					else
						kvs.push(['user', users[annotation.userid].name]);
				}
				
				if ('group' in annotation)
				{
					kvs.push(['group', annotation.group]);
				}
				
				for (var i in kvs) (function(item)
				{
					var row = $('<tr />');
					if (typeof(item) == 'string')
					{
						row.append($('<td />').addClass('separator').attr('colspan',2));
						body.append(row);
						return;
					}
					row.append($('<td />').text(item[0]).addClass('key'));
					var valuetd = $('<td />').addClass('value '+(counter++%2==1?'even':'odd'));
					if (item.length == 3)
					{
						if (item[2] == 'colorpick')
						{
							var span = $('<span />').attr('title',annotation.color).css('background-color',annotation.color).css('height','16px').css('width','40px').css('display','inline-block').click(function() {
								var colorScale = 'default';
								var width = 80;
								if ('type' in annotation)
								{
									if (annotation.type == 'quality')
									{
										
										colorScale = tools['quality'].isQualityAnsiNist() ? 'quality_ansi_nist' : 'quality_std';
									}
									else if (annotation.type == 'wcs')
										colorScale = 'wcs';
									width = 100;
								}
								var pos = span.offset();
								var popup = $('<div />').addClass('ui-widget ui-widget-content').css('position','absolute').css('z-index','1000').css('width',width+'px').css('top',pos.top).css('left',pos.left).css('font-size', '0.7em');
								Pianos4ToolRidge.createColorPicker(popup, function() {
									if ($(this).attr('data-color') != annotation.color)
									{
										var mods = { color: $(this).attr('data-color') };
										if (colorScale != 'default')
											mods.value = parseInt($(this).attr('data-id'));
										var action = new Pianos4Action_ModifyAnnotation(viewWithFocus, annotation, mods);
										action.setLabel('Change '+type+' color');
										self.exec(action);
										remover.remove();
										popup.remove();
									}
								}, colorScale);
								var remover = $('<div />').css('position','absolute').css('z-index','999').width($(document).width()).height($(document).height()).css('top',0).css('left',0).bind('click', function() {
									popup.remove();
									$(this).remove();
								});
								$('body').append(remover).append(popup);
							});
							valuetd.append(span);
						}
						else
						{
							
						}
					}
					else
					{
						valuetd.text(item[1]);
					}
					row.append(valuetd);
					body.append(row);
				})(kvs[i]);
			})(view.getAnnotationById(aid));
			
			var plural = function(n, t) {
				if (n == 1) return t;
				else if (t == 'minutia') return 'minutiae';
				else return t+'s';
			}
			
			headerText += (view.isMark() ? 'Mark' : 'Print')+' view: '+numSel+' '+plural(numSel, multiTypes ? 'annotation' : firstType)+' selected';
		}
		
		if (numSel == 0)
		{
			headerText = 'No properties to show';
		}
		
		var content = $('<div />').append($('<div />').text(headerText));
		if (table)
			content.append(table);
			
		panes.properties.find('.ui-widget-content').html(content);
	}
	
	this.onMouseCaptured = function(tool)
	{
		if (tool.getName() != activeTool)
		{
			$('#'+params.divid+'_tool_selector_'+tool.getName()).next().addClass('ui-state-highlight');
		}
	}
	
	this.onMouseReleased = function(tool)
	{
		if (tool.getName() != activeTool)
			$('.pianos4_toolbox_tool').removeClass('ui-state-highlight');
	}
	
	this.getViewWithFocus = function()
	{
		return viewWithFocus;
	}
	
	this.setMinutiaMarkerSize = function(factor)
	{
		if (mark)
			mark.setMinutiaMarkerSize(factor);
		if (print)
			print.setMinutiaMarkerSize(factor);
	}
	
	this.isReady = function()
	{
		return operations.length == 0 && doingModal == 0;
	}
	
	this.skipNextContextMenu = function()
	{
		$(document).bind('contextmenu', skipNextContextMenuHandler);
	}
	var skipNextContextMenuHandler = function(e)
	{
		e.preventDefault();
		$(document).unbind('contextmenu', skipNextContextMenuHandler);
		return false;
	}
	
	// Returns the currently logged-in user (if 'id' argument is not provided) 
	// or the user whose id is given - this user must exist within the current
	// results working set.
	this.getUser = function(id)
	{
		if (typeof(id) == 'undefined')
			return params.user;
		else
		{
			
			return users[id];
		}
	}
	
	this.getUsers = function()
	{
		var ret = $.extend(true, {}, users);
		if (!(params.user.id in ret))
		{
			var logged = {};
			logged[params.user.id] = params.user;
			$.extend(true, ret, logged);
			ret[params.user.id].logged = true;
		}
		return ret;
	}
	
	this.setPhase = function(phase)
	{
		
		
		if (phase != displayedPhase)
		{
			var applyFunc = function()
			{
				if (params.hotSwitchPhase)
				{
					displayedPhase = phase;
					if (params.v3)
						self.setDataV3(serverData);
					
					panes.toolbar.find('.pianos4_toolbar_phase').removeClass('pianos4_toolbar_phase_active');
					panes.toolbar.find('.pianos4_toolbar_phase_'+phase).addClass('pianos4_toolbar_phase_active');
				}
				else
				{
					self.enterModal();
					
					setTimeout(function() {
					
						// Set up the parameters for the future PiAnoS instance
						var p = $.extend(true, {}, params);
						var previousPhase = p.phase;
						p.phase = phase;
						if (params.v3 && params.user.tutor)
							p.initData = serverData;
						p.linkToPhases = {};
						if (params.phase != 'analysis')
							p.linkToPhases['analysis'] = true;
						if (params.phase == 'evaluation' || params.phase == 'comparison')
							p.linkToPhases['comparison'] = true;
						if (params.phase == 'evaluation')
							p.linkToPhases['evaluation'] = true;

						if (p.loadResults)
							p.loadResults = p.loadResults.replace(new RegExp('=('+previousPhase.toUpperCase()+')&'), '='+p.phase.toUpperCase()+'&');
						if (p.saveResults)
							p.saveResults = p.saveResults.replace(new RegExp('=('+previousPhase.toUpperCase()+')&'), '='+p.phase.toUpperCase()+'&');
						// Destroy the current instance
						layoutObj.destroy();
						$('body').html('<div id="pianos" />');
						pianos.unregisterHandlers();
						// $('*').unbind();
						// $(document).unbind();
						// $(window).unbind();
						delete pianos;
						
						// Create the new instance and run it
						//jQueryUIResetMouseWidgets();
						pianos = new Pianos4(p);
						pianos.init();
					}, 1);
				}
			};
			
			if (params.user.tutor || !lastSaveData.isModified())
			{
				applyFunc();
			}
			else
			{
				var title = 'Confirm exercise step change';
				var message = 'You have unsaved modifications. Do you really want to proceed ?';
				var dlg = $('<div class="pianos4_dialog" />')
					.append($('<div class="pianos4_dialog_main" />').text(message));
					
				self.dialog(dlg, {
					autoOpen: true,
					close: dlg.remove(),
					title: title,
					modal: true,
					buttons: {
						'Yes': function() { $(this).dialog('close'); applyFunc(); },
						'No': function() { $(this).dialog('close'); }
					}
				})
			}
		}	
	}
	
	this.getAnnotationsSummary = function()
	{
		var res = {
			count: 0,
			num_paired_minutiae: 0,
		};
		
		var counted = ['minutiae','areas','ridges'];
		for (var j in counted)
		{
			var atype = counted[j];
			res['num_'+atype] = 0;
		}
		
		for (var i in allViews)
		{
			var view = allViews[i];
			res[view.getType()] = {
				count: 0,
			};

			var counter = 0;
			var data = view.getData();

			for (var j in counted)
			{
				var atype = counted[j];
				
				res[view.getType()]['num_'+atype] = 0;
				for (var k in data[atype])
				{
					var annot = data[atype][k];
					res.count++;
					res[view.getType()]['num_'+atype]++;
					res[view.getType()].count++;
					res['num_'+atype]++;

					if (view.getType() == 'print' && atype == 'minutiae' && 'pairing' in annot)
						res['num_paired_minutiae']++;
				}
			}
		}

		return res;
	}
	
	this.showToolHelp = function(toolname)
	{
		if (!toolname)
			toolname = activeTool;
		
		
		var tool = tools[toolname];

		var title = 'Help on '+tool.getLabel()+' tool';
		var message = 'getHelp' in tool ? tool.getHelp() : $('<p />').text('No help is available');
		
		var dlg = $('<div class="pianos4_dialog" />')
			.append($('<div class="pianos4_dialog_help" />').css('max-height',$(window).height()-140).append(message));

		var helpdiv = $('.pianos4_tools_options ._help');
		var offs = helpdiv.offset();
		var pos = [offs.left + helpdiv.outerWidth(), offs.top];
		
		dlg.dialog({
			autoOpen: true,
			close: function() { dlg.remove(); },
			title: title,
			position: pos,
			width: 500,
			buttons: {
				'Close': function() { $(this).dialog('close'); },
			}
		})
	}
	
	this.showHelp = function(topic)
	{
		var ret = Pianos4Help.getGeneralHelp(topic);
		var title = ret[0];
		var message = ret[1];
		
		var dlg = $('<div class="pianos4_dialog" />')
			.append($('<div class="pianos4_dialog_help" />')	
				.data('pianos4.help',topic)
				.css('max-height',$(window).height()-140)
				.append(message));

		var dlgopts = {
			autoOpen: true,
			close: function() { dlg.remove(); },
			title: title,
			width: 500,
			buttons: {
				'Close': function() { $(this).dialog('close'); },
			}
		};
		
		var previous = $('.pianos4_dialog_help');
		if (previous.length == 1)
		{
			var prevTopic = previous.data('pianos4.help');
			if (prevTopic)
			{
				dlgopts.buttons = {
					'Back': function() { self.showHelp(prevTopic); $(this).dialog('close'); },
					'Close': dlgopts.buttons['Close'],
				};
			}
		}
		
		dlg.dialog(dlgopts);
	}

	this.reLayoutLeftColumn = function()
	{
		layoutToolsPane();
	}
	
	this.clearSelection = function()
	{
		if (mark)
			mark.selectAnnotation(null, true, true);
		if (print)
			print.selectAnnotation(null, true, true);
		self.onSelectionChanged();
	}
	
	this.toggleFullscreen = function(force)
	{
		var wanted;
		if (typeof(force) == 'undefined')
			wanted = !isFullscreen;
		else
			wanted = force;
		
		for (var pane in layoutObj.panes)
		{
			//var pane = layoutObj.panes[i];
			if (pane == 'center')
				continue;
			if (wanted)
				layoutObj.close(pane);
			else
				layoutObj.open(pane);
		}
		
		isFullscreen = wanted;
	}
	
	// This functionality is a "RAII" version of $(document/window).bind() that will remember the
	// handler so that it can be removed when the pianos.unregisterHandlers() method is called. This is because one cannot
	// simply do a $(document).unbind() and expect things to work - jQuery UI (esp. jquery.ui.mouse) and 
	// maybe other libraries/plugins do register global, static callbacks that would be unbound, without
	// possibility of re-registering them again. For the record, we want to unbind everything when the
	// pianos object is destroyed/recreated, so that handlers are not registered twice. Because of this,
	// we could either expect jQuery UI to have a reset() function of some kind - which it hasn't and will
	// probably never have - or do the things cleanly on our end. This is the goal of this function.
	// Note:
	// This fixes the "cannot move slider after step change" bug - this is because jquery.ui.mouse
	// has a 'mouseup' handler that sets 'mouseHandled' to false appropriately, and would not work after
	// the global unbind(), thus preventing any plugin that relied on the mouse to work, e.g. sliders, but
	// also the layout, resizables, etc.
	// Usage:
	//   will call $(document/window).bind(event,handler) if event is a string
	//   will call $(document/window).bind(event[0], event[1], handler) if event is an array of length 2
	// The second form is useful for e.g. keydown events.
	this.registerHandler = function(event, handler, isWindow)
	{
		var target = isWindow ? $(window) : $(document);
		if (typeof(event) == 'string')
			target.bind(event, handler);
		else
			target.bind(event[0], event[1], handler);
		handlerRegistry.push([event,handler,isWindow]);
		return self;
	}
	
	this.unregisterHandlers = function()
	{	
		for (var i in handlerRegistry)
		{
			var item = handlerRegistry[i];
			var target = item[2] ? $(window) : $(document);
			target.unbind(typeof(item[0])=='string'?item[0]:item[0][0], item[1]);
		}
		handlerRegistry = [];
	}
	
	this.setZoomFit = function()
	{
		var z = [];
		if (mark)
			z.push(mark.getZoomFitFactor());
		if (print)
			z.push(print.getZoomFitFactor());
		z.sort();
		setZoomFactor(z[0], true);
	}

	this.getUsersResultsVisibility = function(onlyVisible)
	{
		
		var res = {};

		panes.results.find('table input[type=checkbox]').each(function() {
			var cb = $(this);
			var uid = cb.data('pianos4.userid');
			
			if (onlyVisible)
			{
				if (cb.prop('checked'))
					res[uid] = true;
			}
			else
				res[uid] = cb.prop('checked');
		});
		return res;
	}
	
	this.setFilters = function(newFilters)
	{
		filters = newFilters;
		self.setData({});
		panes.results.data('pianos4.me', null);
		panes.results.find('.table_header_options').append($('<span />').addClass('please_wait').text('Please wait ...'));
		self.load();
	}

	
	// ------------------------------
	// Private variables
	// ------------------------------
	
	var self = this;
	var layoutObj = null;
	var mark = null;
	var print = null;
	var allViews = [];
	var viewWithFocus = null;
	var tools = {};
	var activeTool = null;
	var stack = [];
	var stackPtr = -1;
	var panes = {};
	var screenResolution = null;
	var users = {};
	var operations = { length: 0 };
	var xhrID = 1;
	var doingModal = 0;
	var rightColumn = {
		'history': 100,
		'layers': true,
		'properties': -1.0,
	};
	var defaultRightColumn = $.extend(true, {}, rightColumn);
	rightColumn['history'] = false;
	var displayedPhase = params.phase;
	var serverData = null;
	var handlerRegistry = [];
	var isFullscreen = params.startsFullscreen;
	var lastSaveData = {
		stamp: null,
		stackPtr: -1,
		isModified: function() { return stackPtr != this.stackPtr; }
	};
	var previousConclusions = null;
	var reportingTool = null;
	var filters = null;
	var toggleConclusionsTexts = params.user.tutor ? ['Expand observations','Collapse observations'] : ['Expand the conclusion','Collapse the conclusion'];
	var tutorSVG = null;
	var drawTutorSVG = null;
	var tutorData = {};
	var tutorDataReady = false;
	//var skipNextContextMenu = false;
	
	// The zoom slider (v) goes from 0 to 100
	// The actual zoom factor (z) goes from 2% to 3200%
	// We want a value of 100% zoom when the slider is at the middle point (v=50)
	// Using MATLAB to compute a quadratic logarithmic regression, i.e.
	// > v = [0,50,100]
	// > z = [2,100,3200]
	// > Z = log(z)
	// > c = polyfit(v,Z,2)
	// we find out the values below, so that the zoom factor in % is given by the formula
	// f(x) = exp(c(1)*(x^2)+c(2)*x+c(3))
	var sliderQuadRegVals = [
		-8.925742052568407e-05,
		0.082703331134847,
		0.693147180559945
	];
	// Inverse function: after c' = polyfit(Z,v,2), g(x) = c(1)*log(x)^2+c(2)*log(x)+c(3)
	var sliderQuadRegValsInv = [
		0.223081222576626,
		11.599155816038060,
		-8.147102196497766
	];
	
	// ------------------------------
	// Private functions
	// ------------------------------
	

	var buildUI = function()
	{
		var mainDiv = $('#'+params.divid);
		
		var toolbar = buildToolbar();
		var toolbox = params.showToolbox ? buildToolbox() : null;
		var zoombox = buildZoombox();
		var history = buildHistory();
		var options = buildOptionsPanel();
		var layersp = buildLayersPanel();
		var properties = buildPropertiesPanel();
		var statusBar = buildStatusBar();
		var results = null;
		
		var views = $('<div class="pianos4_views ui-layout-center" />');
		
		var rightCol = $('<div class="pianos4_right_column" />');
		var rightPanes = [];
		if (params.user.tutor)
		{
			results = buildResultsPanel();
			rightCol.append(results);
			panes.results = results;
			rightPanes.push('results');
			rightColumn['results'] = -1;
			defaultRightColumn['results'] = -1;
			rightColumn['properties'] = false;
			defaultRightColumn['properties'] = -1;
		}
		rightCol.append(layersp).append(properties).append(history);
		
		mainDiv
			.addClass('pianos4')
			.data('Pianos4', self)
			.append(makeFloatablePane(toolbar, 'north'))
			.append(views)
			.append(makeFloatablePane($('<div />').append(toolbox).append(options).append(zoombox), 'west'))
			.append(makeFloatablePane(rightCol, 'east'))
			.append(makeFloatablePane(statusBar, 'south'))
			;
		

		// North panel
		panes.toolbar = toolbar;
		
		// West panel
		panes.toolbox = toolbox;
		panes.options = options;
		panes.zoombox = zoombox;
		
		// East panel
		panes.history = history;
		panes.layers = layersp;
		panes.properties = properties;
		rightPanes.push.apply(rightPanes, ['history','layers','properties']);
		$(rightPanes).each(function() {
			var paneName = this.toString();
			var pane = panes[paneName];
			makeWidgetCollapsible(pane, function(value) {
				if (value)
					rightColumn[paneName] = defaultRightColumn[paneName];
				else
					rightColumn[paneName] = false;
				layoutRightColumn();
			});
		});
		for (var pane in rightColumn)
			if (typeof(rightColumn[pane]) == 'boolean' && !rightColumn[pane])
				panes[pane].find('.pianos4_toggle_subpane').removeClass('ui-icon-triangle-1-se').addClass('ui-icon-triangle-1-e');
		
		// South panel
		panes.statusBar = statusBar;
		
		var markDiv = $('<div class="pianos4_mark" />');
		views.append(markDiv);

		mark = new Pianos4View({
			path: '#'+params.divid+' .pianos4_mark',
			type: 'mark',
			div: markDiv,
			image: params.mark,
			imageEditorScript: params.imageEditorScript,
			debug: params.debug,
			userid: params.userid,
			splitByUser: params.user.tutor,
			editToolHotkey: params.editToolHotkey,
			useOtherFeaturesLayer: params.useWCSTool,
			inferEditTool: params.inferEditTool,
			showModifiedMinutiae: params.showModifiedMinutiae,
			updatedMinutiaeTimestamp: params.updatedMinutiaeTimestamp,
			//useH5Canvas: params.useBrushTool,
		});
		allViews.push(mark);
		
		
		if (params.phase == 'comparison' || params.phase == 'evaluation')
		{
			var printDiv = $('<div class="pianos4_print" />');
			views.append(printDiv);
			
			print = new Pianos4View({
				path: '#'+params.divid+' .pianos4_print',
				type: 'print',
				div: printDiv,
				image: params.print,
				imageEditorScript: params.imageEditorScript,
				debug: params.debug,
				userid: params.userid,
				splitByUser: params.user.tutor,
				editToolHotkey: params.editToolHotkey,
				useOtherFeaturesLayer: params.useWCSTool,
				inferEditTool: params.inferEditTool,
				//useH5Canvas: params.useBrushTool,
			});
			allViews.push(print);
			
			mark.dom().addClass('pianos4_view_half');
			print.dom().addClass('pianos4_view_half');
		}
		else
		{
			mark.dom().addClass('pianos4_view_full');
		}
		
		if (mark)
			mark.init();
			
		if (print)
			print.init();
			
		mainDiv.append('<div class="pianos4_floating_panes" />');
		
		if (params.user.tutor)
			buildTutorStatusSVG();
		
		layoutObj = mainDiv.layout({
				defaults: {
					fxName: 'none',
					spacing_open: params.layoutBorderWidth,
					spacing_closed: params.layoutBorderWidth,
					initClosed: params.startsFullscreen,
					//applyDefaultStyles: true,
				},
				center: {
					onresize: function() { onWindowSizeChanged(false); },
				},
				west: {
					size: 90,
				},
				south: {
					size: 24,
				},
			});

		self.registerHandler('beforeunload', onBeforeUnload, true);
		
		// Call onWindowSizeChanged now to perform the initial layout
		onWindowSizeChanged(true);
	}
	
	var buildToolbox = function()
	{
		var toolbox = $('<div class="pianos4_toolbox ui-widget" />');
		toolbox.append($('<div class="ui-widget-header" />').text('Tools'));
		
		var toolboxInner = $('<div class="ui-widget-content" />');

		for (var toolname in tools) (function(tool)
		{
			var checked = '';
			if (activeTool && activeTool == tool.getName())
				checked = ' checked="checked"';
			var radio = $('<input class="pianos4_toolbox_tool" type="radio" id="'+params.divid+'_tool_selector_'+tool.getName()+'" class="pianos4_toolbox_button" name="'+params.divid+'_tool_active"'+checked+' />').attr('title', tool.getLabel());
			var label = $('<label class="pianos4_toolbox_tool" for="'+params.divid+'_tool_selector_'+tool.getName()+'" />').attr('title', tool.getLabel());
			//label.text(tool.getLabel());
			radio.bind('click', function() {
				setActiveTool(tool.getName());
				$(this).blur();
			});
			toolboxInner.append(radio).append(label);
			
			tool.activator = label;
			
			radio.button();
			label.css('background-image', 'url('+params.cssPath+tool.getIcon()+')');
		})(tools[toolname]);
		
		toolbox.append(toolboxInner);

		return toolbox;
	}
	
	var buildZoombox = function()
	{
		var zoombox = $('<div class="pianos4_zoombox ui-widget" />');
		zoombox.append($('<div class="ui-widget-header" />').text('Zoom'));
		
		var zoomboxInner = $('<div class="ui-widget-content" />');
		//var zoombox = $('<span class="">Zoom: <span class="_zoom_value">100.00</span> %</span>');
		var zoomFit = $('<span class="pianos4_zoombox_button">Fit</span>');
		zoomFit.button().bind('click', function() { self.setZoomFit(); });
		var zoom11 = $('<span class="pianos4_zoombox_button">1:1</span>');
		zoom11.button().bind('click', function() { setZoomFactor(1.0); });
		var zoom10x = $('<span class="pianos4_zoombox_button">10x</span>');
		zoom10x.button().bind('click', function() { setZoomMagnify(10.0); });
		
		var setup = null;
		if (params.showPreferences)
		{
			setup = $('<span class="ui-icon ui-icon-gear ui-inline-icon ui-corner-all" />')
				.click(function() {
					setupScreenResolution();
				});
		}
		
		var zoomSlider = $('<div class="pianos4_zoombox_slider" />').slider({
			min: 0,
			max: 100,
			value: 50,
			orientation: 'vertical',
			slide: function(event,ui) {
				var factorPercent = getZoomFactorFromSliderValue(ui.value);
				var factor = factorPercent / 100.0;
				updateZoomSlider(factor, ui.value);
				if (mark)
					mark.setZoomFactor(factor);
				if (print)
					print.setZoomFactor(factor);				
			},
		});
		
		zoomboxInner
			.append(zoomFit)
			.append(zoom11)
			.append(zoom10x)
			.append(setup)
			.append(zoomSlider)
			.append('<div class="pianos4_zoombox_value"><span class="_magnify_value" />x</div>')
			.append('<div class="pianos4_zoombox_value"><span class="_zoom_value" /> %</div>')
			//.append('<div>Slider: <span class="_slider_value" /></div>')
		;
		
		zoombox.append(zoomboxInner);
		
		return zoombox;
	}
	
	var buildResultsPanel = function()
	{
		var panel = $('<div class="pianos4_results ui-widget" />');
		
		panel.append($('<div class="ui-widget-header" />').text('Results'));
		
		var createBtn = function(icon, title, cb, fn) {
			var button = $('<a />').attr('href','#').addClass('button').click(function(e) {
				e.preventDefault();
				cb();
			}).attr('title',title);
			var icon = $('<span />').addClass('ui-state-default ui-corner-all pianos4_icon_box').append($('<span />').addClass('ui-icon').addClass(icon));
			button.append(icon);
			if (fn) fn.apply(button);
			return button;
		};
		
		var controls = $('<div />').addClass('table_header')
			.append($('<div />').addClass('table_header_options')
				.append($('<input />').attr('type','checkbox').prop('disabled',true).click(function() {
					toggleAllResults($(this).prop('checked'));
				}))
				.append(createBtn('ui-icon-triangle-1-n', 'Review previous user', function() { onSelectNextUserResults(true); }))
				.append(createBtn('ui-icon-triangle-1-s', 'Review next user', function() { onSelectNextUserResults(); }))
				.append($('<span />').addClass('please_wait').text('Please wait ...'))
			)
			.append($('<div />').addClass('table_filter_options').css('margin-top','0.2em'));
		;
		
		if (params.getConclusionsScript)
		{
			createBtn('ui-icon-calculator', 'View all observations', function() { onExportResults(); }, function() { $(this).find('.pianos4_icon_box').css('margin-right','1em'); }).insertBefore(controls.find('.please_wait'));
		}
		
		if (params.resultsFilterScript && params.user.tutor && !params.v3)
		{
			createBtn('ui-icon-scissors', 'Filter results ...', function() { onFilterResults(); }, function() { $(this).find('.pianos4_icon_box').css('margin-right','1em'); }).insertBefore(controls.find('.please_wait'));
		}
		
		var table = $('<table />');
		table.append($('<thead />')
			//.append($('<tr />')
			//	.append('<th>All</th><th>Name</th><th>LR</th>')
			//)
		).append('<tbody />');
		
		$('<div class="ui-widget-content" />')
			.appendTo(panel)
			.append(controls)
			.append($('<div />').addClass('table_wrapper').append(table));
		
		return panel;
	}

	var buildHistory = function()
	{
		var history = $('<div class="pianos4_history ui-widget" />');
		
		history.append($('<div class="ui-widget-header" />').text('History'));
		
		$('<div class="ui-widget-content" />')
			.appendTo(history)
			.append($('<div />')
				.append('<ol />')
				//.append('<div />')
			);
		
		return history;
	}
	
	var buildPropertiesPanel = function()
	{
		var props = $('<div class="pianos4_properties ui-widget" />');
		
		props.append($('<div class="ui-widget-header" />').text('Properties'));
		
		var inner = $('<div class="ui-widget-content" />');
		
		props.append(inner);

		return props;
	}
	
	var buildLayersPanel = function()
	{
		var layers = $('<div class="pianos4_layers ui-widget" />');
		
		layers.append($('<div class="ui-widget-header" />').text('Layers'));
		
		var l = ['images','areas','others','ridges','minutiae','pairing','segments','comments'];
		var unchecked = {'pairing':true,'segments':true}
		
		if (params.phase == 'analysis')
		{
			// Do not show 'pairing' and 'segments' during analysis. And yes, Javascript's standard
			// library has been defined by morons - come on, array.remove() ? Without using prototypes ?
			array_remove(l, 'pairing');
			array_remove(l, 'segments');
		}
		if (params.v3)
		{
			array_remove(l, 'areas');
			array_remove(l, 'comments');
		}

		if (!params.useCommentTool)
			array_remove(l, 'comments');
		
		if (!params.useWCSTool)
			array_remove(l, 'others');
			
		if (!params.matcherScript)
			array_remove(l, 'segments');
			
		var inner = $('<div class="ui-widget-content" />');

		for (var i in l) (function(item)
		{
			
			
			var button = $('<div class="button _layer_'+item+' ui-state-default" />').text(params.layerNames[item]);
			
			button.append($('<span class="ui-icon" />'));
			
			button.click(function(e) {
				var on = $(this).hasClass(params.layerOnOffHover[0]);
				self.toggleLayer(item, !on);
			}).bind('mouseover', function(e) { 
				$(this).addClass(params.layerOnOffHover[2]); 
			}).bind('mouseout', function(e) { 
				$(this).removeClass(params.layerOnOffHover[2]); 
			});
			
			if (l[i] in unchecked)
			{
				button.addClass(params.layerOnOffHover[1])
					.find('span').addClass('ui-icon-closethick');
			}
			else
			{
				button.addClass(params.layerOnOffHover[0])
					.find('span').addClass('ui-icon-check');
			}
			
			//inner.append($('<div />').append(button));
			inner.append(button);
			
		})(l[i]); // Create a loop closure to add a "stack frame" - Javascript "scopes" are functions, not blocks
		// so the 'var i=...' statement declares a variable in the current function, regardless of the block.
		// This trick is very well explained here: http://trephine.org/t/index.php?title=JavaScript_loop_closures
		
		layers.append(inner);

		return layers;
	}
	
	var buildToolbar = function()
	{
		var toolbar = $('<div class="pianos4_toolbar ui-widget" />');
		
		var createButton = function(id, text, title) {
			return $('<span />').addClass('pianos4_toolbar_button')
				.attr('id', params.divid+'_toolbar_button_'+id)
				.attr('title', title)
				.text(text)
				.button();
		};
			
		var buttons = [];
		
		buttons.push(createButton('undo', 'Undo', 'Undo the last action')
			.bind('click', function() { self.undo(); }));
		
		buttons.push(createButton('redo', 'Redo', 'Redo the last action')
			.bind('click', function() { self.redo(); }));

		var hasSubmit = false;
		
		if (params.phase != 'evaluation' && !(params.phase in params.linkToPhases))
		{
			if (params.saveResults && !params.user.tutor)
			{
				buttons.push(createButton('save', 'Save', 'Save your results')
					.addClass('pianos4_toolbar_button_spaced')
					.bind('click', function() { self.save(); }));

				if (params.submitResults)
					hasSubmit = true;
			}
			
			if (params.loadResults && params.hasLoadButton)
			{
				buttons.push(createButton('load', params.loadResultsLabel, 'Load results currently saved (revert to last save)')
					.bind('click', function() { self.load(true); }));
			}

			if (!params.user.tutor && params.hasClearButton)
			{
				buttons.push(createButton('clear', 'Clear', 'Clear all results for this step')	
					.bind('click', function() { 
						var action = new Pianos4Action_Batch('Clear');
						action.append(new Pianos4Action_SetData(null)); 
						action.append(new Pianos4Action_UpdateConclusions(getConclusions(), {}, true));
						self.exec(action);
					}));
			}
		}
		else
		{
			// undo.css('visibility','hidden');
			// redo.css('visibility','hidden');
		}

		if (hasSubmit)
		{
			buttons.push(createButton('submit', 'Submit', 'Submit your results and proceed to the next step')
				.addClass('pianos4_toolbar_button_spaced')
				.addClass('pianos4_toolbar_button_submit')
				.bind('click', function() { self.save(true); }));
		}
		
		if (params.reportToolURL)
		{
			buttons.push(createButton('report', 'Report ...', 'Generate various reports')
				.bind('click', function() {
					if (!reportingTool)
						reportingTool = new Pianos4_Report({p4:params});
					reportingTool.run();
				})
			);
		}
		
		var tools = $('<div class="ui-widget-content" />')
			.append('<a href="?" class="pianos4_title">PiAnoS 4</a>')
			.append('<span class="pianos4_loading" />');
			
		for (var i in buttons)
			tools.append(buttons[i]);
		
		if (params.imageEditorScript && params.phase != 'evaluation' && !(params.phase in params.linkToPhases))
		{
			createButton('edit_mark', 'Edit Mark ...', 'Open the image editor for the mark')
				.bind('click', function() { 
					showImageEditor(true);
				}).appendTo(tools);
			
			if (params.phase == 'comparison')
			{
				createButton('edit_print', 'Edit Print ...', 'Open the image editor for the print')
					.bind('click', function() { 
						showImageEditor(false);
					}).appendTo(tools);
			}
		}
		
		if (params.matcherScript && params.phase != 'analysis')
		{
			createButton('matching', 'Matching ...', 'Open the minutiae matcher')
				.bind('click', function() { 
					showMatcher();
				}).appendTo(tools);
		}
		
		// jQuery gotcha: the element must be already part of the DOM for show()/hide() to function 
		// correctly with 'display' other than 'block'.
		$('<span />').addClass('pianos4_unsaved_message _hidden').appendTo(tools).text('Unsaved modifications');
		
		if (params.title)
		{
			if (typeof params.title == 'string')
			{
				$('<span />').addClass('pianos4_exercise_title').appendTo(tools).text(params.title);			
			}
			else
			{
				var genLink = function(item, isPrev) {
					var elem;
					if (item && item.length == 2)
						elem = $('<a />').attr('href',item[1]).attr('title',item[0]).button().click(function() { $(this).attr('href', $(this).attr('href')+'&step='+params.phase); });
					else
						elem = $('<span />').button({disabled:true});
					elem.addClass('pianos4_exercise_title'+(isPrev?'_prev':'_next')).text(isPrev?'<':'>');
					return elem;
				};
				$('<span />').addClass('pianos4_exercise_title_wrap')
					.append(genLink(params.title[0], true))
					.append($('<span />').addClass('pianos4_exercise_title').text(params.title[1]))
					.append(genLink(params.title[2], false))
				.appendTo(tools);
			}
		}
		
		var phases = [
			{ id: 'analysis', label: 'Analysis' },
			{ id: 'comparison', label: 'Comparison' },
			{ id: 'evaluation', label: 'Evaluation' },
		];
		
		var span = $('<span class="pianos4_toolbar_phases" />');
		var previous = true;
		for (var i in phases) (function(phase)
		{
			if (!(phase.id in params.phases))
				return;
				
			if (i>0)
				span.append($('<span class="ui-icon ui-icon-carat-1-e ui-inline-icon" />'));
			
			var p = $('<span />').text(phase.label).addClass('pianos4_toolbar_phase pianos4_toolbar_phase_'+phase.id);
			
			if (phase.id == params.phase)
			{
				p.addClass('pianos4_toolbar_phase_active');
				previous = false;
			}
			else if (previous || params.user.tutor || phase.id in params.linkToPhases)
			{
				p.addClass('pianos4_toolbar_phase_previous');
				p.bind('click', function() {	
					self.setPhase(phase.id);
				});
			}
				
			span.append(p);
		})(phases[i]);
		tools.append(span);
		
		span = $('<a class="pianos4_toolbar_help" href="#" />').click(function(e) {
			e.preventDefault();
			self.showHelp('general');
		});
		span
			.append($('<span />').addClass('ui-state-default ui-corner-all pianos4_icon_box').append($('<span />').addClass('ui-icon ui-inline-icon ui-icon-help')))
		;
		
		tools.append(span);
		

		toolbar.append(tools);
		
		return toolbar;	
	}
	
	var buildStatusBar = function()
	{
		var bar = $('<div class="ui-widget ui-widget-content pianos4_statusbar" />');
		var table = $('<table />').appendTo(bar);
		
		var row1 = $('<tr />').appendTo(table);
		
		$('<td class="pianos4_status" />').text('Initializing ...').appendTo(row1);
		
		var conclusions = $('<td class="pianos4_conclusions" />').appendTo(row1)
			.append($('<div />').css('position','relative')
				.append($('<a href="#" />')
					.append($('<span class="ui-state-default ui-corner-all pianos4_icon_box" />').append($('<span class="ui-icon ui-icon-arrowstop-1-n" />')))
					.append($('<span />').text(toggleConclusionsTexts[0]).css('margin-left','0.5em'))
					.click(function(e) {
						e.preventDefault();
						toggleConclusion();
						$(this).blur();
					})
				)
			)
		;
		
		if (!params.user.tutor)
		{
			// Start of user-mode conclusion panel
			//conclusions.addClass('_user_mode');
			
			// "OK-to-submit" visual indicator
			conclusions.find('div:first a[href=#]').parent().append($('<span />')
					.attr('id','pianos4_conclusion_status')
					.addClass('ui-corner-all ui-state-default')
					.attr('title','Expand this panel to answer all questions')
			);
		
			if (params.phase == 'analysis')
			{
				// General distortion factors
				conclusions.append($('<div />')
					.css('margin-top','1em')
					.append($('<div />').css('font-weight','bold').text('1. General distortion factors observed on the mark :'))
					.append($('<a />').attr('href','#').text('Click here to specify ...').click(function(e) {
						e.preventDefault();
						var content = $(this).next('div');
						var initData = content.data('pianos4');
						if (!initData) initData = [];
						showCheckboxesDialog(
							'General distortion factors observed on the mark',
							Pianos4Constants.conclusions.analysis.degradation, 
							function(values) {
								content.data('pianos4', values);
								onConclusionUpdated();
							},
							initData
						);
					}))
					.append($('<div />').attr('id','pianos4_conclusion_analysis_degradation').addClass('pianos4_conclusion_value_string'))
				);

				// General pattern
				conclusions.append($('<div />')
					.css('margin-top','1em')
					.append($('<div />').css('font-weight','bold').text('2. General pattern :'))
					.append($('<a />').attr('href','#').text('Click here to specify ...').click(function(e) {
						e.preventDefault();
						var content = $(this).next('div');
						var initData = content.data('pianos4');
						showGeneralPatternDialog(
							'General pattern',
							Pianos4Constants.conclusions.analysis.general_pattern, 
							function(data) {
								content.data('pianos4', data);
								onConclusionUpdated();
							},
							initData
						);
					}))
					.append($('<div />').attr('id','pianos4_conclusion_analysis_general_pattern').addClass('pianos4_conclusion_value_string'))
				);

				// Quality of level 1,2,3 details
				conclusions.append($('<div />')
					.css('margin-top','1em')
					.append($('<div />').css('font-weight','bold').text('3. Quality of level 1, 2 and 3 details :'))
					.append($('<a />').attr('href','#').text('Click here to specify ...').click(function(e) {
						e.preventDefault();
						var content = $(this).next('div');
						var initData = content.data('pianos4');
						if (!initData) initData = [];
						showCheckboxesDialog(
							'Level 1-2-3 details',
							Pianos4Constants.conclusions.analysis.details, 
							function(values) {
								content.data('pianos4', values);
								onConclusionUpdated();
							},
							initData,
							true
						);
					}))
					.append($('<div />').attr('id','pianos4_conclusion_analysis_details').addClass('pianos4_conclusion_value_string'))
				);

				// Approach to suitability. This field is hidden, and its value is set automatically
				// based on the value sent by the backend. This value itself comes from the user's
				// profile, and is quite hardcoded to 2 (value for comparison) in the various user
				// creation functions (AppDB::createUserStub and Login with Shibboleth stuff). This
				// exists for legacy reasons (NIJ project) but does not harm if we keep it.
				var menu = $('<select />')
					.attr('id','pianos4_conclusion_analysis_suitability_approach')
					.change(function(e) {
						onConclusionUpdated();
					});
				$('<option />')
					.attr('value','-1')
					.text('Please choose')
					.css('font-style','italic')
					.appendTo(menu);			
				for (var i in Pianos4Constants.conclusions.analysis.suitability_approach)
				{
					var item = Pianos4Constants.conclusions.analysis.suitability_approach[i];
					$('<option />').attr('value',item.id).text(item.label).appendTo(menu);
				}
				conclusions.append($('<div />').css('display','none')
						.css('margin-top','0.7em')
						.append($('<div />').css('font-weight','bold').text('-1. Suitability approach :'))
						.append(menu)
				);

				// Final decision of the analysis stage
				menu = $('<select />')
					.attr('id','pianos4_conclusion_analysis_value')
					.change(function(e) {
						onConclusionUpdated();
					});
				$('<option />')
					.attr('value','-1')
					.text('Please choose')
					.css('font-style','italic')
					.appendTo(menu);			
				for (var i in Pianos4Constants.conclusions.analysis.value)
				{
					var item = Pianos4Constants.conclusions.analysis.value[i];
					$('<option />').attr('value',item.id).text(item.label).appendTo(menu);
				}
				conclusions.append($('<div />')
						.css('margin-top','0.7em')
						.append($('<div />').css('font-weight','bold').text('4. Suitability of the latent mark :'))
						.append(menu)
				);
				
				// menu = $('<select />')
					// .attr('id','pianos4_conclusion_analysis_needed_ridges')
					// .change(function(e) {
						// onConclusionUpdated();
					// });
				// $('<option />')
					// .attr('value','-1')
					// .text('Please choose')
					// .css('font-style','italic')
					// .appendTo(menu);		
				// $('<option />').attr('value',1).text('Yes').appendTo(menu);
				// $('<option />').attr('value',0).text('No').appendTo(menu);
				// conclusions.append($('<div />')
						// .css('margin-top','1em')
						// .append($('<div />').css('font-weight','bold').text('Do you feel the need for ridge tracing ?'))
						// .append(menu)
				// );

			}
			else if (params.phase == 'comparison')
			{
				var menu = $('<select />')
					.attr('id','pianos4_conclusion_comparison_decision')
					.change(function(e) {
						onConclusionUpdated();
					});
				$('<option />')
					.attr('value','-1')
					.text('Please choose')
					.css('font-style','italic')
					.appendTo(menu);			
				for (var i in Pianos4Constants.conclusions.comparison.decision_step1)
				{
					var item = Pianos4Constants.conclusions.comparison.decision_step1[i];
					$('<option />').attr('value',item.id).text(item.label).appendTo(menu);
				}
				conclusions.append($('<div />')
						.css('margin-top','0.7em')
						.append($('<div />').css('font-weight','bold').text('1. Final conclusion :'))
						.append(menu)
				);
				
				menu = $('<select />')
					.attr('id','pianos4_conclusion_comparison_decision_ex')
					.change(function(e) {
						onConclusionUpdated();
					});
				$('<option />')
					.attr('value','-1')
					.text('Please choose')
					.css('font-style','italic')
					.appendTo(menu);			
				for (var i in Pianos4Constants.conclusions.comparison.decision_step2)
				{
					var item = Pianos4Constants.conclusions.comparison.decision_step2[i];
					$('<option />').attr('value',item.id).text(item.label).appendTo(menu);
				}
				conclusions.append($('<div />')
						.css('margin-top','0.7em')
						.append($('<div />').css('font-weight','bold').text('1b. Do you mean ?'))
						.append(menu)
				);
				
				menu = $('<select />')
					.attr('id','pianos4_conclusion_comparison_need_better_known')
					.change(function(e) {
						onConclusionUpdated();
					});
				$('<option />')
					.attr('value','-1')
					.text('Please choose')
					.css('font-style','italic')
					.appendTo(menu);		
				$('<option />').attr('value',2).text('Yes').appendTo(menu);
				$('<option />').attr('value',1).text('Possibly').appendTo(menu);
				$('<option />').attr('value',0).text('No').appendTo(menu);
				conclusions.append($('<div />')
						.css('margin-top','1em')
						.append($('<div />').css('font-weight','bold').text('1c. Would your conclusion change if you had a better (in terms of quality and/or extent) exemplar (known) print ?'))
						.append(menu)
				);

				menu = $('<select />')
					.attr('id','pianos4_conclusion_comparison_l3details')
					.change(function(e) {
						onConclusionUpdated();
					});
				$('<option />')
					.attr('value','-1')
					.text('Please choose')
					.css('font-style','italic')
					.appendTo(menu);		
				$('<option />').attr('value',1).text('Yes').appendTo(menu);
				$('<option />').attr('value',0).text('No').appendTo(menu);
				conclusions.append($('<div />')
						.css('margin-top','1em')
						.append($('<div />').css('font-weight','bold').text('2. Were level 3 features decisive in your decision making ?'))
						.append(menu)
				);

				conclusions.append($('<div />')
					.css('margin-top','1em')
					.append($('<div />').css('font-weight','bold').text('3. Quality and quantity :'))
					.append($('<a />').attr('href','#').text('Click here to specify ...').click(function(e) {
						e.preventDefault();
						var content = $(this).next('div');
						var initData = content.data('pianos4');
						if (!initData) initData = null;
						showSlidersDialog(
							['Quality and quantity', 'About the features that you used in the comparison ...'],
							[['You have selected XXX minutiae in agreement, would you say that this number is ... ?', [0,'low'],[100,'high']],
							['You have annotated that XXX% of the latent print is of medium/high quality, would you say that it is overall of .... quality ?', [0,'low'],[100,'high']]],
							[function(values) {
								content.data('pianos4', values);
								onConclusionUpdated();
							},function(dlg) {
								// Replace the XXX in the questions above by the actual values
								var divs = dlg.find('div div');
								var q1 = $(divs.get(1));
								var q2 = $(divs.get(4));
								// Number of paired minutiae
								var mdata = mark.getData('minutiae');
								var numPaired = 0;
								for (var i in mdata.minutiae)
								{
									var minutia = mdata.minutiae[i];
									if ('pairing' in minutia)
										numPaired++;
								}
								q1.text(q1.text().replace('XXX', numPaired.toString()));
								// % of (medium+good) / (poor+bad) quality surfaces
								var qdata = mark.getData('areas');
								
								var qmap = tools.quality.isQualityAnsiNist() ?
									{1: true, 2: true, 3: true, 4: false, 5: false, 6: false} :
									{1: true, 2: true, 3: false};
								var goodArea = 0.0;
								var badArea = 0.0;
								for (var i in qdata.areas)
								{
									var area = qdata.areas[i];
									if ('type' in area && area.type == 'quality')
									{
										
										var surface = Pianos4Area.getSurface(area);
										if (qmap[area.value])
											goodArea += surface;
										else
											badArea += surface;
									}
								}
								var totalArea = goodArea + badArea;
								if (totalArea <= 0.0) // zero if no quality zones at all, negative if bug (?)
								{
									q2.text('Would you say that the latent print is overall of .... quality ?');
								}
								else
								{
									var goodOT = goodArea / totalArea * 100.0;
									q2.text(q2.text().replace('XXX', goodOT.toFixed(2)));
								}
							}],
							initData
						);
					}))
					.append($('<div />').attr('id','pianos4_conclusion_comparison_qualquant').addClass('pianos4_conclusion_value_string'))
				);
			}
			
			// Case notes label
			var caseNotesQuestionIndex = params.phase == 'analysis' ? 5 : 4;
			conclusions.append($('<div />')
					.css('margin-top','1em')
					.append($('<div />').css('font-weight','bold').text(caseNotesQuestionIndex+'. Case notes :'))
					.append($('<div />').text('Further notes and remarks can be entered in the text box on the right >>'))
			);

			// Case notes (observations) text area
			var observations = $('<td class="pianos4_observations" />').appendTo(row1)
				.append($('<div />')
					.append($('<textarea />').attr('rows',1).attr('cols',20).change(function() {
						onConclusionUpdated();
					}))
				);
				
			// End of user-mode conclusion panel
		}
		else
		{
			// Start of admin-mode conclusion panel
			
			// Since most things displayed here in tutor mode are SVG graphs, we must wait until the 
			// statusbar's DOM is attached to the page's DOM. Therefore, we do nothing here - just leave
			// the bar blank. It will be filled in buildTutorStatusSVG(), called later during buildUI().

			// End of admin-mode conclusion panel
		}
		return bar;
	}

	var buildTutorStatusSVG = function()
	{
		var tr1 = panes.statusBar.find('tr:first');
		var canvas = $('<div />');
		tr1.append($('<td />').attr('rowspan',2).addClass('pianos4_tutor_graphs').append(canvas));
		
		tutorSVG = d3.select('.pianos4_statusbar .pianos4_tutor_graphs div')
			.append("svg:svg")
			//.attr("class", "chart")
			.attr("width", canvas.width())
			.attr("height", canvas.height());
		
		var tr2 = $('<tr />').appendTo(tr1.parent());
		var userData = $('<div />').addClass('pianos4_tutor_userdata');
		tr2.append($('<td />').attr('colspan',2).append(userData));
		
		var complete = function(data)
		{
			tutorSVG.selectAll('*').remove();
			if (!data || data.error)
			{
				var msg = 'Error while loading chart data';
				if (!data)
					msg = 'Results charts not available';
				else if (data.message)
					msg += ': '+data.message;
				tutorSVG.append('text')
					.attr('x', 5)
					.attr('y', 7)
					.attr('fill', 'red')
					.attr('dy', '.35em')
					.text(msg);
			}
			else
			{
				var data = postProcessConclusionData(data.ret);

				var agg = {
					progress: [0, 0, 0, 0, 0, 0, 0],
					mark_value: [0, 0, 0],
					decision: [0, 0, 0, 0, 0],
				};

				var prepareHistogram = function(constants) {
					var values = [];
					for (var i in constants)
						values.push(constants[i].id);
					var from = Math.min.apply(Math, values);
					var to = Math.max.apply(Math, values);
					
					var array = []; 
					for (var i=0;i<=to;++i)
						array.push(0);
					return array;
				}
				agg.degradation = prepareHistogram(Pianos4Constants.conclusions.analysis.degradation);
				
				var unset_mark_values = 0;
				var unset_decisions = 0;
				for (var i in data)
				{
					var d = data[i];
					
					// Exercise progress
					
					agg.progress[d.progress.status]++;
					
					// Mark value
					if (typeof d.analysis.value == "number")
					{
						
						agg.mark_value[d.analysis.value]++;
					}
					else
						unset_mark_values++;
						
					// Final decision
					if (typeof d.comparison.decision == "number")
					{
						var d1 = d.comparison.decision;
						
						var d2 = null;
						var dec = null; // decision combining d1 and d2, in [0..5]
						if (d1 == 0) // exclusion
						{
							dec = d1;
						}
						else if (d1 == 2) // inconclusive
						{
							d2 = d.comparison.decision_ex;
							
							dec = 2;
							if (d2 == 2)
								dec--;
							else
								dec += d2;
						}
						else if (d1 == 1) // identification
						{
							dec = 4;
						}
						
						agg.decision[dec]++;
					}
					else
						unset_decisions++;
				}
				agg.mark_value.unshift(unset_mark_values);
				agg.decision.unshift(unset_decisions);
				
				var prepareStacked = function(vector)
				{
					var ret = [];
					for (var i in vector)
					{
						var d = vector[i];
						if (i==0)
							ret.push({x:d, x0:0})
						else
							ret.push({x:d, x0: ret[i-1].x0+ret[i-1].x});
					}
					return ret;
				}
				
				var stacked_agg = {};
				for (var i in agg)
					stacked_agg[i] = prepareStacked(agg[i]);

				// Color scale for exercise progress
				var progress_colors = d3.scale.ordinal();
				progress_colors.range(['#c7c7c7', // not started -> gray
					'#c2e699', // analysis started -> pale green
					'#78c679', '#78c679',  // analysis completed + comparison started (same states, actually) -> light green
					'#31a354', '#31a354',  // comparison completed + evaluation started (same states, actually) -> medium green
					'#006837']); // evaluation completed -> dark green
				
				var mark_value_colors = d3.scale.ordinal();
				mark_value_colors.range(['#c7c7c7',	// gray -> unset
					Pianos4Constants.getConstantById(3, Pianos4Constants.quality.standard).color, // red -> no value
					Pianos4Constants.getConstantById(1, Pianos4Constants.quality.standard).color,	// green -> suitable for ID
					Pianos4Constants.getConstantById(2, Pianos4Constants.quality.standard).color]);	// orange -> suitable only for exclusion
					
				var decision_colors = d3.scale.ordinal();
				decision_colors.range(['#c7c7c7',	// gray -> unset
					Pianos4Constants.getConstantById(3, Pianos4Constants.quality.standard).color, // red -> exclusion
					'#175987', // darker blue     -> inconclusive, leaning towards exclusion
					'#1f77b4', // blue            -> inconclusive, neutral
					'#2a92da', // lighter blue    -> inconclusive, leaning towards identification
					Pianos4Constants.getConstantById(1, Pianos4Constants.quality.standard).color]); // green -> identification
					
				var tooltip = $('<div />')
					.css('position','absolute')
					.css('z-index',2)
					.css('background-color','yellow')
					.addClass('ui-widget ui-widget-content ui-corner-all')
					.attr('id', 'pianos4_tutor_graph_tooltip')
					.css('padding','0.1em 0.2em')
					.css('font-size','0.7em')
					.css('display','none')
					.appendTo('body');
					
				drawTutorSVG = function(width)
				{
					var graphs = [{
							class_: 'progress',
							data: stacked_agg.progress,
							colors: progress_colors,
							label: 'Progress',
							label_width: 50,
							desc: ['Unset','In analysis','Analysis submitted','In comparison','Comparison submitted','Finished','Finished'],
						},{
							class_: 'mark_value',
							data: stacked_agg.mark_value,
							colors: mark_value_colors,
							label: 'Mark values',
							label_width: 70,
							desc: ['Unset', Pianos4Constants.getConstantById(0, Pianos4Constants.conclusions.analysis.value).label,
								Pianos4Constants.getConstantById(1, Pianos4Constants.conclusions.analysis.value).label,
								Pianos4Constants.getConstantById(2, Pianos4Constants.conclusions.analysis.value).label],
						},{
							class_: 'decision',
							data: stacked_agg.decision,
							colors: decision_colors,
							label: 'Decisions',
							label_width: 60,
							desc: ['Unset', 
								Pianos4Constants.getConstantById(0, Pianos4Constants.conclusions.comparison.decision_step1).label,
								Pianos4Constants.getConstantById(2, Pianos4Constants.conclusions.comparison.decision_step1).label+' - '+Pianos4Constants.getConstantById(2, Pianos4Constants.conclusions.comparison.decision_step2).label,
								Pianos4Constants.getConstantById(2, Pianos4Constants.conclusions.comparison.decision_step1).label,
								Pianos4Constants.getConstantById(2, Pianos4Constants.conclusions.comparison.decision_step1).label+' - '+Pianos4Constants.getConstantById(1, Pianos4Constants.conclusions.comparison.decision_step2).label,
								Pianos4Constants.getConstantById(1, Pianos4Constants.conclusions.comparison.decision_step1).label],
						}
					];
					
					var reservedWidth = 0;
					var graphPixelMargin = 20;
					for (var i in graphs)
					{
						if (i > 0)
							reservedWidth += graphPixelMargin;
						reservedWidth += graphs[i].label_width;
					}
					var graphPixelWidth = Math.max(100.0, (width-reservedWidth)/3.0);
				
					var x = d3.scale.linear()
						.domain([0, data.length])
						.range([0, graphPixelWidth]);
						
					var offset = 0;
					tutorSVG.selectAll('text').remove();
					var updateTooltip = function(desc, num) {
						tooltip.html('');
						tooltip.append($('<span />').text(desc+' ('))
							.append($('<span />').css('font-weight','bold').text(num))
							.append($('<span />').text(' user'+(num == 1 ? '' : 's')+')'));
						var measurer = tooltip.clone().css('opacity','0').appendTo('body').css('top',0).css('left',0);
						var xpos = d3.event.pageX + 10;
						var ttwidth = measurer.width();
						var wwidth = $(window).width()
						if (xpos+ttwidth >= wwidth)
							xpos = wwidth - ttwidth - 10;
						measurer.remove();
						tooltip.css('top', (d3.event.pageY-20)+'px').css('left',(xpos)+'px');
					}
					for (var i in graphs) (function(graph)
					{
						var bar = tutorSVG.selectAll('g.'+graph.class_)
							.data(graph.data);
							
						var bar_new = bar.enter().append('g').classed(graph.class_,true);
						bar_new.append('rect')
								.attr('height', 14)
								.attr('fill', function(d,i) { return graph.colors(i); })
								.on('mouseover', function(d,k) { updateTooltip(graph.desc[k], d.x); tooltip.show(); })
								.on('mousemove', function(d,k) { updateTooltip(graph.desc[k], d.x); })
								.on('mouseout', function(d) { tooltip.hide(); });
						
						tutorSVG.append('text')
							.attr('x', offset)
							.attr('y', 7)
							.attr('dy', '.5em')
							.style('font-size', '0.8em')
							.text(graph.label);
							
						bar
							.attr('transform', function(d) { return 'translate('+(x(d.x0)+offset+graph.label_width)+' 2)'; })
							.select('rect')
								.attr('width', function(d) { return x(d.x) + 'px'; });
						
						offset += graph.label_width + graphPixelWidth + graphPixelMargin;
					})(graphs[i]);
				}
				
				drawTutorSVG(tr1.find('.pianos4_tutor_graphs').width());
				// setTimeout(function() {
					// x.range([0, 1200]);
					// render();
				// }, 2000);
				onTutorDataAvailable('graphs', data);
			}
		}
		
		if (!params.getConclusionsScript)
		{
			complete();
		}
		else
		{
			self.ajax({
				pendingMessage: 'Loading conclusions',
				url: params.getConclusionsScript,
				dataType: 'json',
				success: function(data) {
					complete(data);
				},
				error: function() {
					complete();
				}
			});
		}
	}
	
	var onUsersResultsToggled = function(numChecked)
	{
		
		if (tutorDataReady)
			updateTutorGraphs();
	}

	var onTutorDataAvailable = function(what, data)
	{
		
		
		tutorData[what] = data;
		
		
		var willBeReady = Object.keys(tutorData).length >= 2;
		var gettingReady = !tutorDataReady && willBeReady;
		if (gettingReady)
		{
			tutorDataReady = true;
			var indexData = function(data) {
				var idx = {};
				for (var i in data) {
					var d = data[i];
					var uid = d.user.id;
					
					idx[uid] = d;
				}
				return idx;
			}
			tutorData.results = indexData(tutorData.results);
			tutorData.graphs = indexData(tutorData.graphs);
			
			updateTutorGraphs();
		}
	}
	
	var updateTutorGraphs = function()
	{
		
		
		var vis = self.getUsersResultsVisibility();
		
		var checked = [];
		var numUsers=0;
		for (var uid in vis)
		{
			
			numUsers++;
			var visible = vis[uid];
			var results = tutorData['results'][uid];
			var conclusions = tutorData['graphs'][uid];
			if (visible)
				checked.push(uid);
		}
		
		
		var nl2br = new RegExp(/\n/g);
		var writeUserData = function(element, username, notes)
		{
			element.html('').append($('<div />').css('font-weight','bold').text(username));
			if (notes)
			{
				var lines = notes.split(nl2br);
				for (var i in lines)
					element.append($('<div />').text(lines[i]));
			}
			else 
			{
				var label = notes === false ? 'Please select a single user in the Results pane to show the details.' : 'No case note';
				element.append($('<div />').css('font-style','italic').text(label));
			}
		}
		
		var userData = panes.statusBar.find('.pianos4_tutor_userdata');
		
		if (numUsers == 0)
		{
			writeUserData(userData, 'No users to show', false);
		}
		else if (checked.length == 0)
		{
			writeUserData(userData, 'No results selected', false);
		}
		else if (checked.length == 1)
		{
			var uid = checked[0];
			var results = tutorData['results'][uid];
			var conclusions = tutorData['graphs'][uid];
			
			var concl = conclusions[params.phase];
			writeUserData(userData, conclusions.user.name, concl.notes);
			
		}
		else
		{
			writeUserData(userData, 'Multiple results selected', false);
		}
	}
	
	var setActiveToolByHotkey = function(toolname)
	{
		if (activeTool == toolname)
			return;
			
		var box = $('input#'+params.divid+'_tool_selector_'+toolname);
		
		box.trigger('click');
	}
	
	var registerHotkeys = function()
	{
		// Undo/redo
		self.registerHandler(['keypress','ctrl+z'], function() {
			if (stackPtr >= 0 && stackPtr < stack.length) self.undo(); });
		self.registerHandler(['keypress', 'ctrl+y'], function() {
			if (stackPtr >= -1 && stackPtr < stack.length-1) self.redo(); });

		// Tools
		var toolsHotKeys = {
			'e': 'edit',
			'm': 'minutia',
			'r': 'ridge',
			'a': 'area',
			'q': 'quality',
			'o': 'wcs',
			'p': 'pairing',
			's': 'edit', // alias for the edit tool (s as in 'select')
		};
		for (var i in toolsHotKeys) (function(shortcut, toolname) {
			if (toolname in tools)
				self.registerHandler(['keypress',shortcut], function() { setActiveToolByHotkey(toolname); });
		})(i,toolsHotKeys[i]);
		
		// Toggle panes
		self.registerHandler(['keypress', 'b'], function() { layoutObj.toggle('north'); });
		self.registerHandler(['keypress', 't'], function() { layoutObj.toggle('west'); });
		self.registerHandler(['keypress', 'l'], function() { layoutObj.toggle('east'); });
		self.registerHandler(['keypress', 'c'], function() { layoutObj.toggle('south'); });
		self.registerHandler(['keypress', 'f'], function() { self.toggleFullscreen(); });

		// Misc.
		self.registerHandler(['keydown', 'home'], function() { self.setZoomFit(); }); // oddly enough, 'keypress' doesn't work for the home key ...
	}
	
	var setStatus = function(message)
	{
		var where = '.pianos4_status';	
		panes.statusBar.find(where).text(message);
	}
	
	var buildOptionsPanel = function()
	{
		var options = $('<div class="pianos4_tools_options ui-widget" />');
		options.append($('<div class="ui-widget-header" />').text('Options'));
		var content = $('<div class="ui-widget-content" />');
		content.append($('<div class="_content" />'));
		
		var help = $('<div class="_help" />');
		
		var iconWrap = $('<span class="ui-state-default ui-corner-all pianos4_icon_box" />');
		var icon = $('<span class="ui-icon ui-inline-icon ui-icon-help" />');
		iconWrap.append(icon);
		var helpLink = $('<a href="#" />').bind('click', function(e) {
			e.preventDefault();
			self.showToolHelp(activeTool);
		});
		helpLink.append(iconWrap);
		helpLink.append($('<span class="_help_text"/>').text('Show help'));
		var icon2 = $('<span class="ui-icon ui-inline-icon ui-icon-triangle-1-e pianos4_icon_box" />');
		helpLink.append(icon2);
		help.append(helpLink);
		
		content.append(help);
		options.append(content);
		return options;
	}
	
	var onWindowSizeChanged = function(isInitial)
	{
		/*var win = $(window);
		var availH = win.height()
			- $('#'+params.divid+' .ui-layout-north').outerHeight()
			- $('#'+params.divid+' .ui-layout-resizer-north').outerHeight()
			- $('#'+params.divid+' .ui-layout-resizer-south').outerHeight()
			- $('#'+params.divid+' .ui-layout-south').outerHeight()
		;
		
		var availW = win.width();
		var east = $('#'+params.divid+' .ui-layout-east');
		if (east.hasClass('ui-layout-pane-docked')) availW -= east.outerWidth() - $('#'+params.divid+' .ui-layout-resizer-east').outerWidth();
		*/
		
		
		
		var size = [$('#'+params.divid+' .pianos4_views').width(),$('#'+params.divid+' .pianos4_views').height()];
		
		$('#'+params.divid+' .pianos4_mark').attr('style', 'top:0px;left:0px;height:'+size[1]+'px');
		if (params.phase == 'comparison' || params.phase == 'evaluation')
		{
			$('#'+params.divid+' .pianos4_print').attr('style', 'top:0px;left:'+size[0]/2.0+'px;height:'+size[1]+'px');
		}
		
		if (mark)
			mark.layout();
		if (print)
			print.layout();
		
		layoutToolsPane(size, isInitial);
		layoutRightColumn(size, isInitial);
		layoutStatusBar(isInitial);
	}
	
	var layoutToolsPane = function(size, isInitial)
	{
		if (!size)
			size = [$('#'+params.divid+' .pianos4_views').width(),$('#'+params.divid+' .pianos4_views').height()];
			
		// Adjust the zoombox so that the zoom slider expands to all the available height
		var slider = panes.zoombox.find('.pianos4_zoombox_slider');
		var usedByToolbox = params.showToolbox ? panes.toolbox.outerHeight() : 0;
		var usedByOptions = panes.options.outerHeight();
		var usedByFixedZoombox = panes.zoombox.outerHeight() - slider.outerHeight();
		var avail = size[1] - usedByToolbox - usedByOptions - usedByFixedZoombox - 2; // 2px for the borders
		slider.css('height', avail+'px');
		//
		
		// Call the active tool's callback, if any
		var t = self.getActiveTool();
		if (t && 'onColumnLayout' in t)
			t.onColumnLayout();
	}
	
	var layoutRightColumn = function(size, isInitial)
	{
		if (!size)
			size = [$('#'+params.divid+' .pianos4_views').width(),$('#'+params.divid+' .pianos4_views').height()];
	/*
	var rightColumn = {
		'history': 0.2,
		'layers': 'on',
		'properties': -1.0,
	};
*/
		var availH = size[1];
		var sizes = {};
		var sumOfProps = 0.0;
		
		// Do a first loop to get the height of fixed-size items and compute
		// the sum of proportions needed by the proportionnally-sized items.
		for (var item in rightColumn)
		{	
			
			var value = rightColumn[item];
			var pane = panes[item];
			var isSwitchedOn = typeof(value) != 'boolean' || value;
			
			var content = pane.find('.ui-widget-content');
			if (isSwitchedOn && content.css('display') == 'none')
				content.removeCSS('display');
			
			if (typeof(value) == 'boolean')
			{
				var height;
				if (value)
					height = pane.outerHeight();
				else
					height = pane.find('.ui-widget-header').outerHeight();
				
				if (height <= 2)
					height = 0;
					
				availH -= height;
				sizes[item] = height;
			}
			else if (typeof(value) == 'number' && value != 0.0)
			{
				if (value < 0.0)
				{
					var prop = -value;
					sumOfProps += prop;
				}
			}
		}
		
		// A second loop handles the user-supplied-sized items: their sizes is not
		// given by the content but by a specified value in pixels.
		var heightUsed = 0.0;
		for (var item in rightColumn)
		{
			var value = rightColumn[item];
			if (typeof(value) == 'number' && value > 0.0)
			{
				
				sizes[item] = value;
				heightUsed += value;
			}
		}
		availH -= heightUsed;
		
		// A third loop handles the proportionnaly-sized items: their sizes is
		// calculated a la wxWidgets
		heightUsed = 0.0;
		for (var item in rightColumn)
		{
			var value = rightColumn[item];
			if (typeof(value) == 'number' && value < 0.0)
			{
				
				var prop = -value / sumOfProps;
				var height = prop * availH;
				sizes[item] = height;
				heightUsed += height;
			}
		}
		availH -= heightUsed;
		
		// // This is only true when there is at least one growable widget
		
		// A fourth loop finally assigns the size to all panes
		for (var item in sizes)
		{
			var pane = panes[item];
			var headerHeight = pane.find('>.ui-widget-header').outerHeight();
			var height = sizes[item]-headerHeight;
			var content = pane.find('.ui-widget-content');
			if (height > 2)
				content.outerHeight(height);
			else
				content.css('display','none');
			
			// Quick and dirty hack to have the 
			if (pane.is('.pianos4_results'))
			{
				var hdr = pane.find('.table_header');
				pane.find('.table_wrapper').outerHeight(content.height()-hdr.outerHeight());
			}
		}
	}
	
	var layoutStatusBar = function(isInitial)
	{
		var sp = $('.ui-layout-south');
		
		if (params.user.tutor)
		{
			var parent = $('.pianos4_tutor_graphs>div');
			var size = [parent.width(), Math.max(19,sp.height()-12)];
			//parent.find('svg').width(size[0]).height(size[1]);
			tutorSVG.attr('width', size[0]).attr('height', size[1]);
			if (drawTutorSVG)
				drawTutorSVG(size[0]);
			$('.pianos4_tutor_userdata').height(size[1]-16);
		}
		else
		{
			var parent = $('.pianos4_observations>div');
			var size = [parent.width(), Math.max(13,sp.height()-12)];
			parent.find('textarea').width(size[0]).height(size[1]);
		}
		
		var a = $('.ui-layout-south .pianos4_conclusions a');
		if (sp.height() > 24)
		{
			if (!a.hasClass('_open'))
				toggleConclusion(true);
		}
		else
		{
			if (a.hasClass('_open'))
				toggleConclusion(true);
		}
	}
	
	var toggleConclusion = function(noResize)
	{
		var a = $('.ui-layout-south .pianos4_conclusions a');
		var wasOpen = a.hasClass('_open');
		if (wasOpen)
		{
			a.removeClass('_open').find('span:last').text(toggleConclusionsTexts[0]);
			a.find('.ui-icon').removeClass('ui-icon-arrowstop-1-s').addClass('ui-icon-arrowstop-1-n');
			if (!noResize)
				layoutObj.sizePane('south', 24);
		}
		else
		{
			a.addClass('_open').find('span:last').text(toggleConclusionsTexts[1]);
			a.find('.ui-icon').removeClass('ui-icon-arrowstop-1-n').addClass('ui-icon-arrowstop-1-s');
			if (!noResize)
				layoutObj.sizePane('south', 300);
		}
	}
	
	var onBeforeUnload = function()
	{
		if (!params.user.tutor && lastSaveData.isModified())
			return 'You have unsaved data';
	}
	
	var setZoomMagnify = function(times)
	{
		var factor = times*screenResolution/params.mark.resolution;
		setZoomFactor(factor);
	}

	var setZoomFactor = function(factor, reset)
	{
		if (mark)
			mark.setZoomFactor(factor, null, reset);
		if (print)
			print.setZoomFactor(factor, null, reset);
		updateZoomSlider(factor);
	}
	
	var updateZoomSlider = function(factor, value)
	{
		if (!value)
			value = getSliderValueFromZoomFactor(100.0*factor);
		$('#'+params.divid+' .pianos4_zoombox_slider').slider('value', value);
		$('#'+params.divid+' ._zoom_value').text((100.0*factor).toFixed(2));
		$('#'+params.divid+' ._slider_value').text(value);
		$('#'+params.divid+' ._magnify_value').text((params.mark.resolution*factor/screenResolution).toFixed(2));
	}
	
	var setActiveTool = function(toolname)
	{
		
	
			
		
		if (toolname != activeTool)
		{
			var prevTool = activeTool ? tools[activeTool] : null;
			if (prevTool)
			{	
				if (prevTool.getOptionsPanel && prevTool.getOptionsPanel())
					prevTool.getOptionsPanel().detach();
				else
					panes.options.find('div._content').html('');
				prevTool.deactivate();
			}
			
			activeTool = toolname;
		
			var tool = tools[activeTool];
			tool.activate();
			if (tool.getOptionsPanel && tool.getOptionsPanel())
				panes.options.find('div._content').append(tool.getOptionsPanel());
			else
				panes.options.find('div._content').append($('<div class="pianos4_tool_nooptions" />').text('No options'));
			
			panes.options.find('.ui-widget-header').text(tool.getLabel() + ' tool');
			
			layoutToolsPane();
		}
	}
	
	var getToolbarButton = function(id)
	{
		return panes.toolbar.find('#'+params.divid+'_toolbar_button_'+id);
	}
	
	var updateUI = function()
	{
		getToolbarButton('undo').button('option', 'disabled', stackPtr < 0);
		getToolbarButton('redo').button('option', 'disabled', stackPtr >= stack.length-1);

		var ul = panes.history.find('ol');
		ul.html('');
		
		for (var i in stack) (function(index, item)
		{
			var li = $('<li />');
			li.append($('<span />').text(item.getLabel()));
			if (index == stackPtr)
				li.css('font-weight', 'bold');
			else if (index > stackPtr)
				li.css('opacity', '0.5');
			li.click(function() {
				if (index == stackPtr)
					return;
				var indexCopy = index;
				var stackPtrCopy = stackPtr;
				if (index < stackPtr)
				{
					while (indexCopy++ < stackPtrCopy)
						self.undo(true);
				}
				else if (index > stackPtr)
				{
					while (indexCopy-- > stackPtrCopy)
						self.redo(true);
				}
				updateUI();
			});
			ul.append(li);
		})(i, stack[i]);
		ul.prepend($('<li >').append($('<span />').text('Initial state')).click(function() {
			while (stackPtr >= 0)
				self.undo();
		}));
		if (stackPtr < 0)
			ul.find('li:first').css('font-weight', 'bold');
		
		//panes.history.find('div:last').text('Stack pointer = '+stackPtr.toString());
		
		var loading = $('#'+params.divid+' .pianos4_loading');
			
		if (operations.length > 0)
		{
			loading.removeCSS('visibility');
			var label = 'Working ...';
			for (var op in operations) { if (op=='length') continue; label = operations[op]; break; }
			setStatus(label);
		}
		else
		{
			loading.css('visibility','hidden');
			
			if (doingModal > 0)
				setStatus('Waiting');
			else
				setStatus('Ready');
		}

		var notReady = !self.isReady();
		getToolbarButton('save').button('option', 'disabled', notReady);
		getToolbarButton('load').button('option', 'disabled', notReady);
		getToolbarButton('clear').button('option', 'disabled', notReady);
		getToolbarButton('submit').button('option', 'disabled', notReady);
		
		if (lastSaveData.isModified())
			panes.toolbar.find('.pianos4_unsaved_message').css('visibility','visible');
		else
			panes.toolbar.find('.pianos4_unsaved_message').css('visibility','hidden');
	}

	var getZoomFactorFromSliderValue = function(value)
	{
		return Math.exp(
			sliderQuadRegVals[0]*Math.pow(value,2.0) + 
			sliderQuadRegVals[1]*value +
			sliderQuadRegVals[2]);
	}
	
	var getSliderValueFromZoomFactor = function(factor)
	{
		var ln = Math.log(factor);
		return sliderQuadRegValsInv[0]*Math.pow(ln,2.0) + 
			sliderQuadRegValsInv[1]*ln +
			sliderQuadRegValsInv[2];
	}
	
	var makeFloatablePane = function(pane, where)
	{
		//var dock = $('<button class="pianos4_pane_dockbtn" />');
		var container = pane;//pane.find('.ui-widget-header:first');
		if (container.length == 0)
			container = pane;
		
		//dock.text('Dock/Float').button({icons:{primary:'ui-icon-pin-s'},text:false}).bind('click', function() { toggleFloatPane(pane, where); });
		
		//container.append(dock);
		
		var container = $('#'+params.divid+' .ui-layout-'+where);
		return $('<div class="ui-layout-'+where+' ui-layout-pane-docked" />').append(pane);
	}
	
	var toggleFloatPane = function(pane, where)
	{
		var wrapper = pane.parent();
		
		
		if (wrapper.hasClass('ui-layout-pane-docked'))
		{
			
			
			var pos = wrapper.position();
			var size = [wrapper.width(),wrapper.height()];

			layoutObj.hide(where);
			
			var p = $('<div class="pianos4_floating_pane" />');
			
			p.css('width', size[0])
				.css('height', size[1])
				.css('left', pos.left)
				.css('top', pos.top)
				.css('z-index', 100)
			;

			p.append($('<div class="ui-widget-header" />').text(pane.attr('title')))
				.appendTo('#'+params.divid+' .pianos4_floating_panes');
			
			pane.detach()
				.appendTo(p)
				.find('.pianos4_pane_dockbtn span.ui-icon-pin-s')
					.removeClass('ui-icon-pin-s')
					.addClass('ui-icon-pin-w');
			pane.find('.ui-widget-header').css('cursor','move');
			
			p.draggable({ 
				handle: '.ui-widget-header',
			});
			
			wrapper.removeClass('ui-layout-pane-docked').addClass('ui-layout-pane-floating');
			
			layoutObj.resizeAll();
		}
		else
		{
			
			
			var p = $('#'+params.divid+' .ui-layout-'+where);
			
			
			pane.detach()
				.appendTo(p)
				.find('.pianos4_pane_dockbtn span.ui-icon-pin-w')
					.removeClass('ui-icon-pin-w')
					.addClass('ui-icon-pin-s');
			pane.find('.ui-widget-header').removeCSS('cursor');
			
			wrapper.remove();
			
			layoutObj.show(where);
			

			p.removeClass('ui-layout-pane-floating').addClass('ui-layout-pane-docked');
		}
	}
	
	var makeWidgetCollapsible = function(widget, callback)
	{
		var header = widget.find('>.ui-widget-header');
		var text = header.text();
		header.text('').append(
			$('<span class="ui-icon ui-icon-triangle-1-se ui-inline-icon pianos4_toggle_subpane" />').click(function() {
				var value = $(this).hasClass('ui-icon-triangle-1-e');
				if (value)
					$(this).removeClass('ui-icon-triangle-1-e').addClass('ui-icon-triangle-1-se');				
				else
					$(this).removeClass('ui-icon-triangle-1-se').addClass('ui-icon-triangle-1-e');				
				callback(value);
			})
		).append($('<span />').text(text));
		return widget;
		
	}
	
	var showImageEditor = function(isMark)
	{
		var image = isMark ? mark : print;
		
		image.showImageEditor();
	}
	
	var showMatcher = function()
	{
		var dlg = $(document).find('.pianos4_matcher_dialog');
		if (dlg.length == 0)
		{
			var randomID = 'pianos4_matcher_type_'+(new Date().getTime());
			var mtypes = ['Bozorth', 'Segments', 'Ellipses'];
			var select = $('<select id="'+randomID+'" />');
			for (var i in mtypes)
			{
				var sel = i == 0 ? 'selected="selected"' : '';
				var opt = $('<option '+sel+' value="'+i+'" />').text(mtypes[i]);
				select.append(opt);
			}
			
			var matcherType = $('<div />')
				.append($('<label for="'+randomID+'" />').text('Matcher type'))
				.append($('<span />').append(select));
				
			
			randomID = 'pianos4_matcher_setting1_'+(new Date().getTime());
			var maxSegmentLengthLabel = $('<label for="'+randomID+'" />').text('Max segment length');
			var maxSegmentLengthValue = $('<input type="text" id="'+randomID+'" value="125.0" />');
			var maxSegmentLength = $('<div />')
				.append(maxSegmentLengthLabel)
				.append($('<span />').append(maxSegmentLengthValue));
				
			randomID = 'pianos4_matcher_setting2_'+(new Date().getTime());
			var distanceThresholdLabel = $('<label for="'+randomID+'" />').text('Distance threshold');
			var distanceThresholdValue = $('<input type="text" id="'+randomID+'" value="10.0" />');
			var distanceThreshold = $('<div />')
				.append(distanceThresholdLabel)
				.append($('<span />').append(distanceThresholdValue));

			randomID = 'pianos4_matcher_setting3_'+(new Date().getTime());
			var angleThresholdLabel = $('<label for="'+randomID+'" />').text('Angle threshold');
			var angleThresholdValue = $('<input type="text" id="'+randomID+'" value="15.0" />');
			var angleThreshold = $('<div />')
				.append(angleThresholdLabel)
				.append($('<span />').append(angleThresholdValue));

			randomID = 'pianos4_matcher_setting4_'+(new Date().getTime());
			var onlyUnpairedLabel = $('<label for="'+randomID+'" />').text('Only unpaired');
			var onlyUnpairedValue = $('<input type="checkbox" id="'+randomID+'" value="on" />');
			var onlyUnpaired = $('<div />')
				.append(onlyUnpairedLabel)
				.append($('<span />').append(onlyUnpairedValue));

			randomID = 'pianos4_matcher_result_'+(new Date().getTime());
			var reslabel = $('<label for="'+randomID+'" />').text('Result');
			var resvalue = $('<span id="'+randomID+'" class="pianos4_matcher_dialog_result" />');
			var result = $('<div />')
				.append(reslabel)
				.append($('<span />').append(resvalue));
			
			var pairing = $('<button />').text('Apply pairing');
			pairing.button({disabled:true}).click(function() {
				var data = dlg.data('pianos4');
				var views = [mark, print];
				// Create a batch action that pairs all minutiae
				var action = new Pianos4Action_Batch();
				for (var i in data.pairing)
				{
					var pair = data.pairing[i];
					var a = new Pianos4Action_PairMinutiae(
						mark.getAnnotation('minutia', pair[0]),
						print.getAnnotation('minutia', pair[1]),
						Pianos4Annotation._s_pairIds++
					);
					action.append(a);
				}
				self.exec(action);
				self.toggleLayer('pairing', true);
			});
			
			var showseg = $('<button />').text('Show segments');
			showseg.button({disabled:true}).click(function() {
				var data = dlg.data('pianos4');
				
				mark.setSegments(data.segments);
				print.setSegments(data.segments);
				self.toggleLayer('segments', true);
			});
			
			dlg = $('<div />')
				.addClass('pianos4_matcher_dialog')
				.append(matcherType)
				.append(maxSegmentLength)
				.append(distanceThreshold)
				.append(angleThreshold)
				.append(onlyUnpaired)
				.append(result)
				.append($('<div />').append(pairing).append(showseg))
			;
			
			self.dialog(dlg, {
				autoOpen: false,
				modal: true,
				title: 'Matcher',
				width: 400,
				buttons: {
					'Run matcher': function(e) {
						$(e.target).button('option','disabled',true);
						select.attr('disabled','disabled');
						pairing.button('option','disabled',true);
						showseg.button('option','disabled',true);
						resvalue.addClass('ui-state-disabled').text('Please wait ...');

						var qsparams = [];

						var idx = $(this).find('select').val();
						
						var type = mtypes[idx].toLowerCase();
						qsparams.push(['type', type])
						
						if (type == 'segments')
						{
							var msl = parseFloat(maxSegmentLength.find('input').val());
							if (msl > 0)
								qsparams.push(['maxSegmentLength',msl]);
							var distth = parseFloat(distanceThreshold.find('input').val());
							if (distth > 0)
								qsparams.push(['distanceThreshold',distth]);
							var angleth = parseFloat(angleThreshold.find('input').val());
							//if (angleth > 0)
							qsparams.push(['angleThreshold',angleth]);
						}
						
						var qstemp = [];
						$.each(qsparams, function() { qstemp.push([this[0],encodeURIComponent(this[1])].join('=')); });
						var qs = qstemp.join('&');
						
						// Primary filter to filter out minutiae that are not shown
						// (only active for tutors)
						var filter_visible = null;
						if (params.user.tutor)
						{
							var visibility = self.getUsersResultsVisibility();
							filter_visible = function(a)
							{
								
								return visibility[a.userid];
							}
						}
						// Secondary filter to filter out minutiae that are already paired
						// (not active if checkbox is unchecked)
						var filter_unpaired = onlyUnpairedValue.attr('checked') ? function(a) {
							return !('pairing' in a);
						} : null;
						// Global filter that comprises the two above
						var filter = function(a) {
							var res = true;
							if (res && filter_visible)
								res &= filter_visible(a);
							if (res && filter_unpaired)
								res &= filter_unpaired(a);
							return res;
						};
						var post = self.getData('minutiae', filter);
						
						self.ajax({
							pendingMessage: 'Running the matcher',
							type: 'post',
							processData: false,
							contentType: 'application/json',
							url: params.matcherScript+'&'+qs,
							data: JSON.stringify(post),
							success: function(data) {
								try
								{
									if (data.success)
									{
										var text = data.ret.score.normalized + ' (' +
											data.ret.score.absolute + ' / ' + data.ret.score.reference + ')';
										resvalue
											.removeClass('ui-state-disabled ui-state-error ui-state-default')
											.addClass('ui-state-highlight')
											.text(text);
										dlg.data('pianos4', data.ret);
										pairing.button('option','disabled',data.ret.pairing.length==0);
										showseg.button('option','disabled',data.ret.segments.length==0);
									}
									else
									{
										var msg = data.message.substr(0,5) == 'ERROR' ? data.message : 'ERROR: '+data.message;
										resvalue
											.removeClass('ui-state-disabled ui-state-highlight ui-state-default')
											.addClass('ui-state-error')
											.text(msg);
									}
								}
								catch (e)
								{
									var msg = 'ERROR: invalid JSON data returned by the server';
									resvalue
										.removeClass('ui-state-disabled ui-state-highlight ui-state-default')
										.addClass('ui-state-error')
										.text(msg);
								}
								$(e.target).button('option','disabled',false);
								select.removeAttr('disabled');
							},
							error: function(jqXHR, textStatus, errorThrown) {
								resvalue
									.removeClass('ui-state-disabled ui-state-highlight ui-state-default')
									.addClass('ui-state-error')
									.text('HTTP error');
								$(e.target).button('option','disabled',false);
								select.removeAttr('disabled');
							},
						});
					},
					'Close': function() {
						$(this).dialog('close');
					},
				}
			});
		}
		
		dlg.find('.pianos4_matcher_dialog_result')
			.removeClass('ui-state-disabled ui-state-error ui-state-highlight ui-state-default')
			.addClass('ui-state-default')
			.text('Click \'run\' to start');
		dlg.data('pianos4', null);
		
		dlg.find('button')
			.button('option','disabled',true);
		
		dlg.dialog('open');
	}
	
	var toggleUserResults = function(uid, on, silenceEvent)
	{
		var me = panes.results.data('pianos4.me');
		
		
		
		var muid = me.users[uid];
		if (muid.status != on)
		{
			
			if (mark) mark.setUserResultsVisible(uid, on);
			if (print) print.setUserResultsVisible(uid, on);
			
			muid.status = on;
			me.numChecked = on ? me.numChecked+1 : me.numChecked-1;
			
			
			panes.results.find('.table_header_options>input[type=checkbox]').prop('indeterminate', me.numChecked != 0 && me.numChecked != me.numUsers);
		}
		else
		{
			
		}
		
		if (!silenceEvent)
			onUsersResultsToggled(me.numChecked);
	}
	
	var toggleAllResults = function(on)
	{
		var me = panes.results.data('pianos4.me');
		
		for (var uid in users)
		{
			var muid = me.users[uid];
			if (muid.status != on)
			{
				muid.dom.prop('checked', on);
				toggleUserResults(uid, on, true);
			}
		}
		onUsersResultsToggled(me.numChecked);
	}
	
	var updateResultsPanel = function(initiallyHidden)
	{
		
		
		// "Local static" variable (i.e. equivalent to 'static <type> <var>' in C)
		var me = panes.results.data('pianos4.me');
		if (!me)
		{
			// This serves as a cache that persists between calls to this function
			me = {
				numUsers: 0,
				users: {},
			};
			for (var uid in users) 
			{
				++me.numUsers;
				me.users[uid] = { status: !initiallyHidden, dom: null };
			};
			me.numChecked = initiallyHidden ? 0 : me.numUsers;
			panes.results.data('pianos4.me', me);
			
			panes.results.find('.table_header_options .please_wait').remove();
			var mainCB = panes.results.find('.table_header_options>input[type=checkbox]');
			mainCB.prop('checked',!initiallyHidden).prop('disabled',false);
			
			if (initiallyHidden)
			{
				var tooltip = $('<div />')
					.addClass('ui-tooltip ui-widget ui-corner-all ui-widget-content')
					.css('z-index','1000')
					.css('text-align','center')
					.css('font-size','0.8em')
					.append($('<div />')
						.addClass('ui-tooltip-content').text('Results are initially hidden'))
					.append($('<div />')
						.addClass('ui-tooltip-content').text('Use the checkbox to show/hide them'))
					// .append($('<div />')
						// .css('font-size','0.8em')
						// .addClass('ui-tooltip-content').text('(Click to dismiss)'))
					.click(function() { if (tooltip) { tooltip.remove(); tooltip=null; }})
					.appendTo('body')
					.position({at:'center left', of:mainCB, my:'right'});
				setTimeout(function() {
					if (tooltip) { 
						tooltip.hide({	
							effect: 'fade',
							complete: function() {
								tooltip.remove();
								tooltip = null;
							}
						})
					}
				}, 5000);
			}
		}
		
		var filter_opts = panes.results.find('.table_filter_options');
		if (filters)
		{
			var items = [];
			var helper = new Pianos4_FilterDialog(null, filters);
			for (var i in filters)
			{
				if (helper.isItemActive(i))
					items.push(i);
			}
			if (items.length)
			{
				filter_opts.text('Filtering by '+items.join(',')).append(
					$('<span />').css('margin-left','0.5em')
						.append('[')
						.append($('<a />').attr('href','#').text('stop filtering').click(function(e) {
							e.preventDefault();
							self.setFilters({});
						}))
						.append(']'));
			}
			else
			{
				filter_opts.text('');
			}
		}
		else
		{
			filter_opts.text('');
		}
		
		var hdr = panes.results.find('.table_header_options');
		var cb = hdr.find('>checkbox');
		
		var k = 0;
		var classes = ['even', 'odd'];
		for (var i in users) (function(uid)
		{
			var user = users[uid];
			var tr = $('<tr />').addClass(classes[k++%2]);
			var cb = $('<input />').attr('type','checkbox').prop('checked',!initiallyHidden);
			cb.bind('click', function() {
				toggleUserResults(uid, $(this).prop('checked'));
			}).data('pianos4.userid',uid);
			tr.append($('<td />').append(cb));
			tr.append($('<td />').text(user.name));
			var lr = $('<td />');
			if (user.lr)
				lr.text(user.lr);
			else
				lr.append('<span class="ui-icon ui-icon-cancel ui-state-disabled" />');
			tr.append(lr);
			var table = panes.results.find('table>tbody');
			table.append(tr);
			me.users[uid].dom = cb;
		})(i);
		
		onUsersResultsToggled(me.numChecked);
	}
	
	var onSelectNextUserResults = function(backwards)
	{
		var table = panes.results.find('table>tbody');
		var checked = table.find('input[type=checkbox]:checked');
		var current = null, next = null;
		
		if (checked.length == 1)
		{
			current = checked;
			if (backwards)
			{
				next = current.parents('tr').prev('tr').find('input[type=checkbox]');
				if (next.length == 0)
					next = table.find('input[type=checkbox]:last');
			}
			else
			{
				next = current.parents('tr').next('tr').find('input[type=checkbox]');
				if (next.length == 0)
					next = table.find('input[type=checkbox]:first');
			}
			if (next[0] == current[0]) // only one user in the result set: current and next are necessarily the same
				return;
		}
		else if (checked.length == 0)
		{
			//current = [];
			next = backwards ? table.find('input[type=checkbox]:last') : table.find('input[type=checkbox]:first');
			if (next.length == 0) // no elements at all in the result set
				return;
		}
		else
		{
			var theOneWhoStays = $(checked[backwards ? checked.length-1 : 0]);
			current = checked.not(theOneWhoStays);
			//next = [];
		}
		
		if (current)
		{
			current.each(function() {
				$(this).prop('checked', false);
				
				toggleUserResults($(this).data('pianos4.userid'), false);
			});
		}
		if (next)
		{
			next.each(function() {
				$(this).prop('checked', true);
				
				toggleUserResults($(this).data('pianos4.userid'), true);
			});
		}
	}
	
	var onDataLoaded = function(data, useAction)
	{
		serverData = data;
		
		if (params.phase == 'analysis' && !params.v3 && params.defaultSuitabilityApproach)
		{
			// auto-set the analysis conclusion approach if it was not initially present in the
			// result set. This method has the disadvantage of forcing this approach even when
			// the user has explicitly unset the value in the conclusions box, has saved and then
			// reloads. (This is however a rare scenario: if the user wishes to change the approach
			// she would select another option, rather than unsetting).
			if (!('observations' in data))
				data.observations = {};
			if (!('mark-suitability-approach' in data.observations))
				data.observations['mark-suitability-approach'] = params.defaultSuitabilityApproach;
		}
		
		if (!useAction)
		{
			if (params.v3)
				self.setDataV3(data);
			else
				self.setData(data);
		}
		else
		{
			var action = new Pianos4Action_SetData(data, params.v3);
			self.exec(action);
		}
	}
	
	var getConclusionFields = function()
	{
		var fields = [
			['string', $('.pianos4_observations textarea'), 'conclusion', false],
		];
		
		if (params.phase == 'analysis')
		{
			fields.push(['data', $('#pianos4_conclusion_analysis_degradation'), 'mark-degradation']);
			fields.push(['data', $('#pianos4_conclusion_analysis_general_pattern'), 'mark-gp']);
			fields.push(['data', $('#pianos4_conclusion_analysis_details'), 'mark-details']);
			fields.push(['dropdown', $('#pianos4_conclusion_analysis_suitability_approach'), 'mark-suitability-approach']);
			fields.push(['dropdown', $('#pianos4_conclusion_analysis_value'), 'mark-value']);
			//fields.push(['dropdown', $('#pianos4_conclusion_analysis_needed_ridges'), 'mark-needed-ridges']);
		}
		else if (params.phase == 'comparison')
		{
			fields.push(['dropdown', $('#pianos4_conclusion_comparison_decision'), 'comparison-decision']);
			fields.push(['dropdown', $('#pianos4_conclusion_comparison_decision_ex'), 'comparison-decision-ex']);
			fields.push(['dropdown', $('#pianos4_conclusion_comparison_need_better_known'), 'comparison-need-better-known']);
			fields.push(['dropdown', $('#pianos4_conclusion_comparison_l3details'), 'comparison-l3details']);
			fields.push(['data', $('#pianos4_conclusion_comparison_qualquant'), 'comparison-qualquant']);
		}
		
		
		
		return fields;
	}
	
	var getConclusions = function(returnFields)
	{
		var conclusions = {
			length: 0
		};

		var fields = getConclusionFields();
		
		var mandatory = {};
		
		for (var i in fields)
		{
			var field = fields[i];
			var isMandatory = field.length <= 3 || field[3];
			var provided = false;
			
			switch (field[0])
			{
			case 'string':
				var value = field[1].val();
				if (value.length) {
					conclusions[field[2]] = value;
					provided = true;
				};
				break;
			case 'dropdown':
				var value = field[1].val();
				var ivalue = parseInt(value);
				if (value.length && ivalue >= 0) {
					conclusions[field[2]] = ivalue;
					provided = true;
				}
				break;
			case 'data':
				var data = field[1].data('pianos4');
				if (typeof(data) != 'undefined') {
					conclusions[field[2]] = data;
					provided = true;
				}
				break;
			default:
				
				break;
			}
			
			if (isMandatory)
				mandatory[field[2]] = false;

			if (provided)
			{
				conclusions.length++;
				if (isMandatory)
					mandatory[field[2]] = true;
			}
			
		}

		if (params.phase == 'comparison' && (!('comparison-decision' in conclusions) || conclusions['comparison-decision'] != 2))
		{	
			var toRemove = ['comparison-decision-ex', 'comparison-need-better-known'];
			for (var i in toRemove)
			{
				var item = toRemove[i];
				if (item in mandatory)
					delete mandatory[item];
				if (item in conclusions) 
				{
					
					delete conclusions[item];
					conclusions.length--;
				}
			}
			fields = fields.filter(function(item) { return toRemove.indexOf(item[2]) < 0; });
		}
		
		var numMandatory = 0;
		var numMandatoryAnswered = 0;
		for (var i in mandatory)
		{
			numMandatory++;
			if (mandatory[i])
				numMandatoryAnswered++;
		}
		
		
		
		conclusions.isOK = conclusions.length == fields.length;
		conclusions.mandatoryOK = numMandatory == numMandatoryAnswered;
		
		if (returnFields)
			return [conclusions, fields];
		else
			return conclusions;
	}
	
	var onConclusionUpdated = function(noUndoRedo)
	{
		
		if (params.phase != 'analysis' && params.phase != 'comparison')
			return;
		
		var stuff = getConclusions(true);
		var conclusions = stuff[0];
		var fields = stuff[1];

		if (!noUndoRedo)
		{
			
			var action = new Pianos4Action_UpdateConclusions(previousConclusions, $.extend(true, {}, conclusions));
			self.exec(action);
		}
		else
		{
			previousConclusions = $.extend(true, {}, conclusions);
		}
		
		if (params.phase == 'analysis')
		{
			if ('mark-degradation' in conclusions)
				setConclusionString($('#pianos4_conclusion_analysis_degradation'), conclusions['mark-degradation'], function(value) {
					return Pianos4Constants.getItemById(Pianos4Constants.conclusions.analysis.degradation, value).label;
				});
			else
				$('#pianos4_conclusion_analysis_degradation').html('');
				
			if ('mark-gp' in conclusions)
				setConclusionString($('#pianos4_conclusion_analysis_general_pattern'), conclusions['mark-gp'][0], function(value) {
					return Pianos4Constants.getItemById(Pianos4Constants.conclusions.analysis.general_pattern, value).label;
				});
			else
				$('#pianos4_conclusion_analysis_general_pattern').html('');
				
			if ('mark-details' in conclusions)
				setConclusionString($('#pianos4_conclusion_analysis_details'), conclusions['mark-details'], function(value) {
					var sub = value < 200 ? 'Level 1' : (value < 300 ? 'Level 2' : 'Level 3');
					return sub+': '+Pianos4Constants.getItemById(Pianos4Constants.conclusions.analysis.details[sub], value).label;
				});
			else
				$('#pianos4_conclusion_analysis_details').html('');
			
			if (!('mark-suitability-approach' in conclusions))
			{
				$('#pianos4_conclusion_analysis_value').attr('disabled',true);
			}
			else
			{
				if (conclusions['mark-suitability-approach'] == 1)
				{
					// Using approach #1 -> show only options 0 and 1, remove previous answer if it was 2
					if ('mark-value' in conclusions && conclusions['mark-value'] == 2)
					{
						$('#pianos4_conclusion_analysis_value').val(-1);
						
						conclusions.length--;
						delete conclusions['mark-value'];
					}
					$('#pianos4_conclusion_analysis_value option[value=2]').remove();
				}
				else
				{
					// Using approach #2 -> show all 3 options (0, 1 and 2), leave previous answer unmodified
					if ($('#pianos4_conclusion_analysis_value option[value=2]').length == 0)
					{
						$('<option />').attr('value',2).text(Pianos4Constants.getItemById(Pianos4Constants.conclusions.analysis.value, 2).label).insertAfter($('#pianos4_conclusion_analysis_value option[value=1]'));
					}
				}
				$('#pianos4_conclusion_analysis_value').removeAttr('disabled');
			}
		}
		else if (params.phase == 'comparison')
		{
			if (!('comparison-decision' in conclusions) || conclusions['comparison-decision'] != 2)
			{
				$('#pianos4_conclusion_comparison_decision_ex').parent().hide();
				$('#pianos4_conclusion_comparison_need_better_known').parent().hide();
			}
			else
			{
				$('#pianos4_conclusion_comparison_decision_ex').parent().show();
				$('#pianos4_conclusion_comparison_need_better_known').parent().show();
			}
			
			if ('comparison-qualquant' in conclusions)
				$('#pianos4_conclusion_comparison_qualquant').text('You answered this question');
			else
				$('#pianos4_conclusion_comparison_qualquant').html('');
		}
		


		var color;
		if (!conclusions.mandatoryOK)
			color = '#d62728';
		else if (!conclusions.isOK)
			color = '#ff7f0e';
		else
			color = '#2ca02c';
		
		// In analysis, one question is actually hidden and answered automatically - we do
		// not want it to appear in the number of questions to be answered, so we reduce
		// the counts by one.
		var numAnswered = conclusions.length;
		var numFields = fields.length;
		
		if (params.phase == 'analysis')
		{
			numAnswered = Math.max(0,numAnswered-1);
			numFields = Math.max(0,numFields-1);
		}
		
		var label = numAnswered+' / '+numFields;
		 $('#pianos4_conclusion_status')
			.text(label)
			.css('color',color)
			.css('border-color',color)
			.data('pianos4.status',[numAnswered,numFields])
		;
	}
	
	var setConclusionString = function(destination, values, formatFunc)
	{
		formatFunc = formatFunc || function(v) { return v; };
		
		var numAdded = 0;
		destination.html('');
		if (values.length)
		{
			for (var i in values)
			{
				if (numAdded > 0)
					destination.append(', ');
				destination.append(formatFunc(values[i]));
				numAdded++;
			}
		}
		else
		{
			destination.html('None');
		}
	}
	
	var showCheckboxesDialog = function(title, items, callback, initValue, sectionsAreRadios)
	{
		initValue = initValue || [];
		
		var sections = $.isArray(items) ? {'':items} : items;
		
		var intro = null;
		if ($.isArray(title)) {
			intro = title[1];
			title = title[0];
		}
		
		var dlgopts = {
			autoOpen: true,
			modal: true,
			title: title,
			close: function() {
				$(this).remove();
			},
			buttons: {
				'OK': function() { 
					var data = [];
					$(this).find('input').each(function() {
						if ($(this).is(':checked'))
							data.push(parseInt($(this).val()));
					});
					var dataValidated = true;
					if (sectionsAreRadios)
					{
						var groups = {};
						var groupsToProcess = {}
						for (var title in sections)
						{
							groups[title] = false;
							groupsToProcess[title] = 0;
						}
							
						for (var i in data)
						{	
							var groupOK = null;
							for (var title in groupsToProcess)
							{
								try
								{
									var dummy = Pianos4Constants.getItemById(items[title], data[i])
									groupOK = title;
									break;
								}
								catch (e)
								{
								}
							}
							
							if (groupOK)
							{
								groups[title] = true;
								delete groupsToProcess[title];
							}
						}
						
						for (var title in sections)
							if (!groups[title])
								dataValidated = false;
					}
					
					if (!dataValidated)
					{
						var errdiv = $(this).find('.ui-state-error');
						if (errdiv.length == 0)
							errdiv = $('<div />').addClass('ui-state-error ui-corner-all').css('margin','0.5em').css('padding','0.3em').appendTo($(this));
						errdiv.html('Please fill in all categories');
					}
					else
					{
						$(this).dialog('close');
						callback(data);
					}
				},
				'Cancel': function() { 
					$(this).dialog('close');
				},
			},
		};
		
		var div = $('<div />');
		var id_base = 'pianos4_cbdlg_opt_'+(new Date().getTime())

		if (intro)
			div.append($('<div />').text(intro));
			
		for (var title in sections)
		{
			var section = sections[title];
			var container = title ? $('<div />').css('font-weight','bold').css('margin-top','0.5em').text(title).appendTo(div) : div;
			
			for (var i in section)
			{
				var item = section[i];
				var id = id_base + '_' + item.id;
				var label = $('<label />').attr('for',id).text(item.label);
				var cb = $('<input />').attr('id',id).attr('value',item.id);
				if (sectionsAreRadios)
				{
					cb.attr('type','radio')
						.attr('name', id_base + '_' + title.replace(/[^a-zA-Z0-9]+/g,''));
				}
				else
				{
					cb.attr('type','checkbox');
				}
				var idx = initValue.indexOf(item.id);
				if (idx >= 0)
					cb.attr('checked','checked');
				div.append($('<div />').append(cb).append(label));
			}
		}
		
		if (!sectionsAreRadios)
			div.append($('<div />').css('font-style','italic').css('font-size','0.9em').css('margin-top','1em').text('Note: it is acceptable to choose none of the above (just leave all boxes unchecked and press OK)'));
			
		var dlg = $('<div />').addClass('pianos4_dialog').append(div);
		
		self.dialog(dlg, dlgopts);
	}
	
	var showSlidersDialog = function(title, items, callback, initValue)
	{
		items = $.isArray(items) ? items : [items];
		
		if (!initValue) {
			initValue = [];
			for (var i in items) initValue.push(null);
		}
		
		

		var intro = null;
		if ($.isArray(title)) {
			intro = title[1];
			title = title[0];
		}
		
		var initCallback = null;
		var finalCallback = null;
		if (callback)
		{
			if ($.isArray(callback))
			{
				initCallback = callback[1];
				finalCallback = callback[0];
			}
			else
				finalCallback = callback;
		}
		
		var dlgopts = {
			autoOpen: true,
			modal: true,
			title: title,
			close: function() {
				$(this).remove();
			},
			buttons: {
				'OK': function() { 
					var data = [];
					$(this).find('.ui-slider').each(function() {
						data.push($(this).slider('value'));
					});
					$(this).dialog('close');
					if (finalCallback)
						finalCallback(data);
				},
				'Cancel': function() { 
					$(this).dialog('close');
				},
			},
		};
		
		var div = $('<div />');
		var id_base = 'pianos4_sldrdlg_opt_'+(new Date().getTime())

		if (intro)
			div.append($('<div />').text(intro));
			
		for (var i in items)
		{
			var item = items[i];
			
			
			var ival = typeof(initValue[i]) == 'number' ? initValue[i] : (item[1][0]+item[2][0])/2;
			
			var slideropt = {
				min: item[1][0],
				max: item[2][0],
				value: ival,
			};
			
			var container = $('<div />').css('font-weight','bold').css('margin-top','1.5em').text(item[0]).appendTo(div);
			
			var slider = $('<div />').css('margin','0.5em').slider(slideropt).appendTo(div);
			
			var labels = $('<div />').css('position','relative').css('text-align','center').appendTo(div);
			labels.append($('<span />').css('position','absolute').css('left','0').text(item[1][1]))
				.append($('<span />').text('average').css('color','white'))
				.append($('<span />').css('position','absolute').css('right','0').text(item[2][1]));
		}
		
		var dlg = $('<div />').addClass('pianos4_dialog').append(div);
		
		if (initCallback)
			initCallback(dlg);
			
		self.dialog(dlg, dlgopts);
	}

	var showGeneralPatternDialog = function(title, items, callback, initValue)
	{
		initValue = initValue || [[], false];

		var intro = null;
		if ($.isArray(title)) {
			intro = title[1];
			title = title[0];
		}
		
		// Sort items by their order of appearance in the initial data, if applicable.
		// (because not only selection matters, but ordering as well)
		var vs = initValue[0];
		items.sort(function(a, b) {
			var aidx = vs.indexOf(a.id);
			var bidx = vs.indexOf(b.id);
			if (aidx >= 0 && bidx >= 0)
				return aidx - bidx;
			else if (aidx >= 0)
				return -1;
			else if (bidx >= 0)
				return 1;
			else
				return a.id - b.id;
		});
		
		var div = $('<div />');
		var id_base = 'pianos4_gpdlg_'+(new Date().getTime())

		if (intro)
			div.append($('<div />').text(intro));
			
		div.append($('<div />').text('Please select one or multiple general pattern(s) that apply to the mark. You can also re-order your selection by dragging the images from left (most probable) to right (least probable)'));
		
		div.append($('<div />').css('font-style', 'italic').text('Note: please answer this question regardless of the confidence you may have regarding the pattern - this is the object of the next question about the quality of Level 1 details.'));

		var buttons = $('<div />').appendTo(div);
		
		var cursel = $('<div />')
			.append($('<span />').text('Your current choice(s):').css('margin-right', '1em'))
			.append($('<span />'))
			.appendTo(div);
		
		var getSel = function(labels)
		{
			var items = [];
			buttons.find('.ui-state-highlight').each(function() {
				var data = $(this).data('pianos4');
				var elem = labels ? data.label : data.id;
				items.push(elem);
			});
			return items;
		}
		
		var updateSel = function()
		{
			var items = getSel(true);
			var span = cursel.find('span:last')
			if (items.length)
				span.css('font-style', 'normal').text(items.join(', '));
			else
				span.text('None').css('font-style', 'italic');
		}
		
		var imgsize = 128;
		var frameheight = 150;
		var used_width = 0;
		for (var i in items) (function(item) {
			var frame = $('<span />').css('display', 'inline-block').width(imgsize).height(frameheight);
			var image = $('<img />').attr('src', item.url).width(imgsize).height(imgsize);
			var label = $('<span />').css('display', 'inline-block').width(imgsize).css('text-align', 'center');
			label.text(item.label);
			frame.append(image);
			frame.append(label);
			//frame.button();
			var button = $('<div >').css('margin', '1em').css('display', 'inline-block').css('padding', '1em').addClass('ui-state-default ui-corner-all').css('cursor', 'pointer').hover(
				function() { $(this).addClass('ui-state-hover'); },
				function() { $(this).removeClass('ui-state-hover'); }
			).click(function(e) {
				$(this).toggleClass('ui-state-highlight');
				updateSel();
			});
			if (initValue[0].indexOf(item.id) >= 0)
				button.addClass('ui-state-highlight');
			button.append(frame);
			button.data('pianos4', item);
			buttons.append(button);
			used_width += imgsize+button.outerWidth(true);
		})(items[i]);
		buttons.sortable();

		var question = $('<div />')
			.append($('<span />').text('Do you feel there is something distinctive about the pattern that makes it stand out ?').css('margin-right','1em'))
			.append($('<select name="peculiar">').append('<option value="no">No</option>').append('<option value="yes">Yes</option>'))
			.appendTo(div);
		if (initValue[1])
			question.find('option[value=yes]').prop('selected', true);
		updateSel();
		
		var dlgopts = {
			autoOpen: true,
			modal: true,
			title: title,
			close: function() {
				$(this).remove();
			},
			width: used_width + 20,
			buttons: {
				'OK': function() { 
					var data = [getSel(), question.find('select').val()=='yes'];
					
					var dataValidated = data[0].length > 0;
					
					if (!dataValidated)
					{
						var errdiv = $(this).find('.ui-state-error');
						if (errdiv.length == 0)
							errdiv = $('<div />').addClass('ui-state-error ui-corner-all').css('margin','0.5em').css('padding','0.3em').appendTo($(this));
						errdiv.html('Please select at least one general pattern');
					}
					else
					{
						$(this).dialog('close');
						callback(data);
					}
				},
				'Cancel': function() { 
					$(this).dialog('close');
				},
			},
		};
		
		var dlg = $('<div />').addClass('pianos4_dialog').append(div);
		
		self.dialog(dlg, dlgopts);
	}
	
	var calculateScreenResolution = function(obj)
	{
		
		var diag = obj.diag * 2.54;
		var ratio = obj.width/obj.height;
		var height = Math.sqrt(diag*diag/(1+ratio*ratio));
		var res = obj.height / height;
		return res * 2.54;
	}
	
	var setupScreenResolution = function()
	{
		var dlgopts = {
			autoOpen: true,
			modal: true,
			width: 450,
			title: 'Configure screen resolution',
			close: function() {
				$(this).remove();
			},
			buttons: {
				'OK': function() { 
					var r = onChange();
					if (!r)
					{
						errdiv.html('The resolution you entered is not valid').show();
						return;
					}
					params.screenResolution = r;
					screenResolution = typeof(r) == 'object' ? calculateScreenResolution(r) : r;
					updateZoomSlider(mark.getZoomFactor());
					$(this).dialog('close');
					
					if (params.preferencesScript)
					{
						self.ajax({
							pendingMessage: 'Saving prefs',
							url: params.preferencesScript,
							data: {
								action: 'set',
								key: 'screenResolution',
								value: r,
							},
							success: function(data) {
								if (('status' in data && data.status == 'ok') || ('error' in data && !data.error))
								{
									// Don't do anything - preferences are saved, that's what we care about
								}
								else
								{
									alert('Warning: preferences could not be saved.\nThey will last only for this session.');
								}
							},
							error: function() {
								alert('Warning: preferences could not be saved.\nThey will last only for this session.');
							},
						});
					}
				},
				'Cancel': function() { 
					$(this).dialog('close');
				},
			},
		};
		
		var div = $('<div />');
		var id_base = 'pianos4_resoldlg_opt_'+(new Date().getTime())

		div.append($('<div />').text('For the magnification factor (e.g. 10x) to be accurate, PiAnoS needs to know the resolution (in pixels per inch) of your screen. You can specify it either directly or by entering the "resolution" used by your operating system (e.g. 1280x1024) and your monitor\'s diagonal size (e.g. 19"). The default is 72 dpi.'));
		
		var pane1 = $('<div />').addClass('ui-widget ui-widget-content ui-corner-all').css('padding','1em').css('margin-top','1em')
			.append($('<div />').css('font-weight','bold')
				.append($('<input />').attr('type','radio').attr('id', id_base+'_direct').attr('name',id_base).attr('value',0))
				.append($('<label  />').attr('for', id_base+'_direct').text('I know the resolution of my screen in dots per inch')))
			.append($('<div />').css('margin-top','0.5em')
				.append($('<input />').attr('type','text').attr('id', id_base+'_dpi').attr('size',6))
				.append($('<span  />').text(' dpi'))
				.append($('<span  />').css('color','#666666').css('font-style','italic').css('font-size','0.9em').css('margin-left','2em').text('(e.g. 72 dpi)')))
		;
			
		var pane2 = $('<div />').addClass('ui-widget ui-widget-content ui-corner-all').css('padding','1em').css('margin-top','1em')
			.append($('<div />').css('font-weight','bold')
				.append($('<input />').attr('type','radio').attr('id', id_base+'_calc').attr('name',id_base).attr('value',1))
				.append($('<label  />').attr('for', id_base+'_calc').text('I want to have it calculated')))
			.append($('<div />').css('margin-top','0.5em')
				.append($('<span  />').text('Display resolution: '))
				.append($('<input />').attr('type','text').attr('id', id_base+'_width').attr('size',5))
				.append($('<span  />').text(' x '))
				.append($('<input />').attr('type','text').attr('id', id_base+'_height').attr('size',5))
				.append($('<span  />').css('color','#666666').css('font-style','italic').css('font-size','0.9em').css('margin-left','2em').text('(e.g. 1280 x 1024)')))
			.append($('<div />').css('margin-top','0.5em')
				.append($('<span  />').text('Monitor size: '))
				.append($('<input />').attr('type','text').attr('id', id_base+'_diag').attr('size',3))
				.append($('<span  />').text(' inches'))
				.append($('<span  />').css('color','#666666').css('font-style','italic').css('font-size','0.9em').css('margin-left','2em').text('(e.g. 19")')))
		;

		div.append(pane1).append(pane2);
		
		if (typeof(params.screenResolution) == 'object')
		{
			pane2.find('#'+id_base+'_width').val(params.screenResolution.width);
			pane2.find('#'+id_base+'_height').val(params.screenResolution.height);
			pane2.find('#'+id_base+'_diag').val(params.screenResolution.diag);
			var r = calculateScreenResolution(params.screenResolution);
			pane1.find('#'+id_base+'_dpi').val(r.toFixed(2));
			pane2.find('#'+id_base+'_calc').attr('checked','checked');
			pane1.find('input[type=text]').attr('disabled','disabled');
		}
		else
		{
			pane1.find('#'+id_base+'_dpi').val(params.screenResolution.toFixed(2));
			pane1.find('#'+id_base+'_direct').attr('checked','checked');
			pane2.find('input[type=text]').attr('disabled','disabled');
			pane2.find('#'+id_base+'_width').val(screen.width);
			pane2.find('#'+id_base+'_height').val(screen.height);
		}
		
		div.find('input[type=radio]').change(function() {
			var value = $(this).val();
			if (value == 0)
			{
				pane1.find('input[type=text]').removeAttr('disabled');
				pane2.find('input[type=text]').attr('disabled','disabled');
			}
			else
			{
				pane1.find('input[type=text]').attr('disabled','disabled');
				pane2.find('input[type=text]').removeAttr('disabled');
			}
		});
		
		pane1.find('input[type=text]').change(function() {
			onChange();
		});

		pane2.find('input[type=text]').change(function() {
			onChange();
		});
		
		var onChange = function() {
			if (pane1.find('input[type=text]').attr('disabled') == 'disabled')
			{
				var obj = {
					width: parseInt(pane2.find('#'+id_base+'_width').val()),
					height: parseInt(pane2.find('#'+id_base+'_height').val()),
					diag: parseInt(pane2.find('#'+id_base+'_diag').val()),
				};
				if (obj.width > 0 && obj.height > 0 && obj.diag > 0)
				{
					var r = calculateScreenResolution(obj);
					pane1.find('#'+id_base+'_dpi').val(r.toFixed(2));
					return obj;
				}
				else
				{
					pane1.find('#'+id_base+'_dpi').val('N/A');
					return null;
				}
			}
			else
			{
				var v = parseFloat(pane1.find('#'+id_base+'_dpi').val());
				if (v > 0.0)
					return v;
				else
					return null;
			}
		};
		
		var errdiv = $('<div />').hide().addClass('ui-state-error ui-corner-all').css('margin-top','1em').appendTo(div);
		
		var dlg = $('<div />').addClass('pianos4_dialog').append(div);
		
		self.dialog(dlg, dlgopts);
	}
	
	var onExportResults = function()
	{
		
		var complete = function(data)
		{
			var dlgopts = {
				autoOpen: true,
				modal: false,
				width: 800,
				title: 'All conclusions',
				close: function() { $(this).remove(); },
				buttons: {
					'Close': function () { $(this).dialog('close'); }
				},
			};
			
			var div = $('<div />');
			var id_base = 'pianos4_resultdlg_opt_'+(new Date().getTime());
			
			
			if (!data || data.error)
			{
				div.addClass('ui-state-error').css('padding','0 0.5em').append($('<h3 />').text('error'));
				if (data && data.message)
					div.append($('<pre />').text(data.message));
			}
			else
			{
				var d;
				if (filters)
					d = data.ret.filter(function(a) { return filterAssignment(a); });
				else
					d = data.ret;
				createUsersResultGrid(div, d);
			}

			var dlg = $('<div />').addClass('pianos4_dialog').css('max-height', $(window).height()-140).append(div);
			
			self.dialog(dlg, dlgopts);
		}
		
		self.ajax({
			pendingMessage: 'Loading conclusions',
			url: params.getConclusionsScript,
			dataType: 'json',
			success: function(data) {
				complete(data);
			},
			error: function() {
				complete();
			}
		});
	}

	// The getConclusion script returns ALL conclusions in a not-so-handy format (that is, we get 
	// the last saved AND submitted data for each step, as arrays). This function re-formats its
	// input into a friendly object, getting rid of the non-latest step (so you get the submitted
	// values if any, or the last saved if any, or nothing at all). This is done by the helper
	// function findLatestResultIndex().
	// One must not forget that the values of the questions, as stored in the database, are string
	// representations of JSON objects. This is because the DB schema does not place any restriction
	// on the kind of data an answer to a question has. Therefore, they are received as *strings*
	// (even though they are transported as a JSON object of higher level), and must be decoded
	// here to be usable directly. This is done by the helper function getStepField().
	var postProcessConclusionData = function(data)
	{
		var findLatestResultIndex = function(data, step)
		{
			var stepidx = -1;
			for (var i=0;i<data.numresults;++i)
			{
				var phase = data.ressteps[i];
				if (phase != step)
					continue;
				else
					stepidx = i;
			}
			return stepidx;
		}
			
		var getStepField = function(data, field, stepidx)
		{
			if (stepidx >= 0)
			{
				try {
					var jsonSrc = data[field][stepidx];
					return JSON.parse(jsonSrc);
				} catch (e) {
					return null;
				}
			}
			return null;
		}
		
		if (filters)
			filteredData = data.filter(function(a) { return filterAssignment(a); });
		else
			filteredData = data;
		
		var ret = [];
		for (var i in filteredData)
		{
			var d = filteredData[i];
			var astep = findLatestResultIndex(d, 'Analysis');
			var cstep = findLatestResultIndex(d, 'Comparison');
			ret.push({
				id: parseInt(d.id,10),
				progress: d.progress,
				user: d.user,
				createdon: d.createdon,
				updatedon: d.updatedon,
				analysis: {
					notes: getStepField(d, 'conclusions', astep),
					validated: d['restypes'][astep] == 'Submission',
					updatedon: d['resupdates'][astep],
					value: getStepField(d, 'mark_values', astep),
					degradation: getStepField(d, 'mark_degradations', astep),
					details: getStepField(d, 'mark_details', astep),
				},
				comparison: {
					notes: getStepField(d, 'conclusions', cstep),
					validated: d['restypes'][cstep] == 'Submission',
					updatedon: d['resupdates'][cstep],
					decision: getStepField(d, 'decisions', cstep),
					decision_ex: getStepField(d, 'decisions_ex', cstep),
					need_better_known: getStepField(d, 'comparison_need_better_knowns', cstep),
					l3details: getStepField(d, 'comparison_l3details', cstep),
					qualquant: getStepField(d, 'comparison_qualquants', cstep),
				},
			});
		}
		return ret;
	}
	
	var createUsersResultGrid = function(selector, data)
	{
		
		var nl2br = new RegExp(/\n/g);
		var writeConclusion = function(concl, cell)
		{
			if (concl)
			{
				var lines = concl.split(nl2br);
				for (var i in lines)
					cell.append($('<div />').text(lines[i]));
			}
			else
			{
				cell.append($('<div />').css('font-style', 'italic').text('No data'));
			}
		}
		
		selector.qtable({
			data: postProcessConclusionData(data),
			columnMode: 'mapping',
			columns: [
				{
					title: 'User',
					render: function(data, i, j, cell) {
						cell.append($('<a />').text(data[i].user.name).attr('href','#').bind('click', function(e) {
							e.preventDefault();
							var dlg = $(this).parents('.pianos4_dialog');
							if (dlg.length)
								dlg.dialog('close');
							var udata = panes.results.data('pianos4.me');
							
							
							for (var uid in udata.users)
							{
								var u = udata.users[uid];
								if (uid == data[i].user.id && !u.status)
								{
									u.dom.prop('checked', true);
									toggleUserResults(uid, true);
								}
								else if (uid != data[i].user.id && u.status)
								{
									u.dom.prop('checked', false);
									toggleUserResults(uid, false);
								}
							}
							
						}));
					},
				},{
					title: 'Status',
					render: function(data, i, j, cell) {
						cell.text(data[i].progress.label);
					},
				},{
					title: 'Analysis',
					render: function(data, i, j, cell) {	
						var d = data[i];
						var mark_value = d.analysis.value;
						var mark_value_label=null;
						
						if (mark_value !== null)
							mark_value_label = Pianos4Constants.getConstantById(mark_value, Pianos4Constants.conclusions.analysis.value).label;
						else
							mark_value_label = 'Value not set';
						var mark_value_colors = {
							0: '#d62728', // no value => red
							1: '#2ca02c', // suitable for ident => green
							2: '#ff7f0e', // suitable for excl only => orange
						};
						var mark_value_color = (mark_value in mark_value_colors) ? mark_value_colors[mark_value] : '#000000';
						cell.append($('<div />').css('font-weight','bold').css('color',mark_value_color).text(mark_value_label));
						writeConclusion(d.analysis.notes, cell);
					},
				},{
					title: 'Conclusion',
					render: function(data, i, j, cell) {
						var d = data[i];
						var decision = d.comparison.decision;
						
						if (decision !== null)
						{
							decision_label = Pianos4Constants.getConstantById(decision, Pianos4Constants.conclusions.comparison.decision_step1).label;
							var ds2 = d.comparison.decision_ex;
							if (ds2)
								decision_label += ' - ' + Pianos4Constants.getConstantById(ds2, Pianos4Constants.conclusions.comparison.decision_step2).label;
						}
						else
						{
							decision_label = 'Decision not set';
						}
						var decision_colors = {
							0: '#d62728', // exclusion => red
							1: '#2ca02c', // identification => green
							2: '#1f77b4', // inconclusive => blue
						};
						var decision_color = (decision in decision_colors) ? decision_colors[decision] : '#000000';
						cell.append($('<div />').css('font-weight','bold').css('color',decision_color).text(decision_label));
						writeConclusion(d.comparison.notes, cell);
					},
				}
			],
		});
	}
	
	var onFilterResults = function()
	{
		
		
		if (filters === null)
			filters = {};
		var dlg = new Pianos4_FilterDialog(params.resultFilterScript, filters, self.setFilters);
		dlg.show();
	}
	
	// Returns true if the given assignment passes the filter specified in the 
	// 'filters' member (this is set by the function above)
	var filterAssignment = function(a)
	{	
		// In the results sent by the server, we have several members of interest:
		// - user: contains the user's name -> used by user filters
		// - progress: contains the status of the assignment -> used by progress filters
		// - observations: contains the textual observations and answers to questions
		//                 at the current step (phase). This is important: in Analysis, 
		//                 we get analysis conclusions, and in comparison, comparison
		//                 conclusions.
		// - analysis: in comparison phase, this additionnal member (absent in analysis)
		//             contains the conclusions of the analysis phase (these are the values
		//             we obtain in the 'observations' member for this assignment in analysis mode.
		// - comparison: same, but the other way around. Note that the server will not send
		//             this field for the moment (but nothing prevents us from assuming it will
		//             be the case in the future)
		
		// 'user', 'group' and 'progress' are always in the result data, so the filters
		// can always be applied
		if ('user' in filters && filters.user.length)
			if (a.user.name.indexOf(filters.user) < 0)
				return false;
		if ('group' in filters && filters.group.length)
			if (a.user.groups.indexOf(filters.group) < 0)
				return false;
		if ('progress' in filters && filters.progress >= 0)
			if (parseInt(a.progress.status) < filters.progress)
				return false;
		
		// Where to find data for the analysis and comparison step depends on the phase
		// being shown in the interface, as explained above.
		var aconcl=null, cconcl=null;
		if (params.phase == 'analysis')
		{
			aconcl = ('observations' in a && 'mark-value' in a.observations) ? a.observations['mark-value'] : -1;
			cconcl = ('comparison' in a && 'comparison-decision' in a.comparison) ? a.comparison['comparison-decision'] : -1;
		}
		else
		{
			aconcl = ('analysis' in a && 'mark-value' in a.analysis) ? a.analysis['mark-value'] : -1;
			cconcl = ('observations' in a && 'comparison-decision' in a.observations) ? a.observations['comparison-decision'] : -1;
		}
		
		// Filters for the analysis and comparison decisions cannot be applied if the information
		// is missing. We thus have to choose one of:
		// - keep the results that do not have the information specified (e.g. if analysis filter
		//   is set to 'no value', analysis results that do not have this information are kept)
		//   (i.e. NULL => true)
		// - discard the results (i.e. NULL => false)
		// The second solution is more intuitive because one expects the filter to keep only those
		// records that match, not the 'undefined' ones. Thus, if a such filter is specified, we
		// dis
		
		if ('analysis' in filters && filters.analysis >= 0) // If filter is active
			if (aconcl < 0 || aconcl !== filters.analysis)  //   If data is missing OR data does not match filter
				return false;								//     discard result
		if ('comparison' in filters && filters.comparison >= 0)
			if (cconcl < 0 || cconcl !== filters.comparison)
				return false;
				
		return true;
	}
}


/* pianos4_action.js */
/*
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/.
*/


// ----------------------------
// Generic actions
// ----------------------------

/**
 * @constructor
 */
 
function Pianos4Action_Batch(displayName)
{
	var actions = [];
	var selectionNeedsUpdate = false;
	
	this.append = function(action)
	{
		actions.push(action);
		
		action.inBatch = this;
		return this;
	}
	
	this.getName = function()
	{
		return 'batch';
	}
	
	this.getLabel = function()
	{
		if (displayName)
			return displayName;
			
		var d = {};
		var s = '';
		for (var i=0;i<actions.length;++i)
		{
			var l = actions[i].getLabel();
			if (l in d)
				continue;
			if (s.length)
				s += ', ';
			s += l;
			d[l] = true;
		}
		if (s.length)
			return s;
		else
			return 'Empty batch action';
	}
	
	this.redo = function()
	{
		preRun('redo');
		
		for (var i=0;i<actions.length;++i)
			actions[i].redo();
		
		postRun('redo');
	}

	this.undo = function()
	{
		preRun('undo');
		
		for (var i=0;i<actions.length;++i)
			actions[actions.length-i-1].undo();
		
		postRun('undo');
	}
	
	this.setSelectionNeedsUpdate = function()
	{
		selectionNeedsUpdate = true;
	}
	
	var preRun = function(mode)
	{
	}
	
	var postRun = function(mode)
	{
		if (selectionNeedsUpdate)
		{
			selectionNeedsUpdate = false;
			pianos.onSelectionChanged(true);
		}
	}
}

/**
 * @constructor
 */
function Pianos4Action_Custom(redoFunc, undoFunc)
{
	

	this.getName = function()
	{
		return 'custom';
	}
	
	this.getLabel = function()
	{
		return 'Custom action';
	}
	
	this.redo = function()
	{
		if (redoFunc)
			redoFunc(this);
	}

	this.undo = function()
	{
		if (undoFunc)
			undoFunc(this);
	}
}

// ----------------------------
// Annotations (common)
// ----------------------------


/**
 * @constructor
 */
function Pianos4Action_CreateAnnotation(view, type, annotation)
{
	
	
	
	this.getName = function()
	{
		return 'createannotation';
	}
	
	this.getLabel = function()
	{
		return 'Create '+type;
	}
	
	this.redo = function()
	{
		switch (type) 
		{
		case 'minutia': view.addMinutia(annotation); break;
		case 'ridge': view.addRidge(annotation); break;
		case 'area': view.addArea(annotation); break;
		case 'comment': view.addComment(annotation); break;
		default:  break;
		}
	}

	this.undo = function()
	{
		// We must de-select the annotation if it is selected. However, we delay the 
		// screen update of the global selection if we are in a batch. The batch itself
		// will do the updating
		if (view.isSelected(annotation))
		{
			if ('inBatch' in this && this.inBatch)
			{
				view.unselectAnnotation(annotation.id, true);
				this.inBatch.setSelectionNeedsUpdate();
			}
			else
				view.unselectAnnotation(annotation.id);
		}
		
		switch (type) 
		{
		case 'minutia': view.removeMinutia(annotation); break;
		case 'ridge': view.removeRidge(annotation); break;
		case 'area': view.removeArea(annotation); break;
		case 'comment': view.removeComment(annotation); break;
		default:  break;
		}
	}
}


/**
 * @constructor
 */
 
function Pianos4Action_ModifyAnnotation(view, annotation, newValues)
{
	
	
	this.getName = function() { return 'modannot'; }
	
	this.getLabel = function() { return label ? label : 'Modify annotation' }
	this.setLabel = function(lbl) { label = lbl; }
	
	this.redo = function()
	{
		annotation.modified = new Date();
		view.updateAnnotation(annotation, newValues);
	}

	this.undo = function()
	{
		annotation.modified = currentModTime;
		view.updateAnnotation(annotation, oldValues);
	}

	var oldValues = {};
	var label = null;
	var currentModTime = null;
	
	(function() {
		for (var key in newValues)
		{
			// Those fields are not mutable through this functor (pairing is actually a
			// mutable field but one should use the dedicated action)
			
			oldValues[key] = annotation[key];
		}
		
		currentModTime = annotation.modified;
	})();
}


/**
 * @constructor
 */
 
function Pianos4Action_DeleteAnnotation(view, id)
{
	
	
	this.getName = function() {	return 'deleteannotation' };
	this.getLabel = function() { return 'Delete annotation' };
	
	this.redo = function()
	{
		if (unpair) 
			unpair.redo();
		
		// We must de-select the annotation if it is selected. However, we delay the 
		// screen update of the global selection if we are in a batch. The batch itself
		// will do the updating
		if (view.isSelected(annotation))
		{
			if ('inBatch' in this && this.inBatch)
			{
				view.unselectAnnotation(id, true);
				this.inBatch.setSelectionNeedsUpdate();
			}
			else
				view.unselectAnnotation(id);
		}
		
		switch (type) 
		{
		case 'minutia': view.removeMinutia(annotation); break;
		case 'ridge': view.removeRidge(annotation); break;
		case 'area': view.removeArea(annotation); break;
		case 'comment': view.removeComment(annotation); break;
		default:  break;
		}
	}

	this.undo = function()
	{
		switch (type) 
		{
		case 'minutia': view.addMinutia(annotation); break;
		case 'ridge': view.addRidge(annotation); break;
		case 'area': view.addArea(annotation); break;
		case 'comment': view.addComment(annotation); break;
		default:  break;
		}
		
		if (unpair) 
			unpair.undo();
	}
	
	var ret = view.getAnnotationById(id);
	var type = ret[0];
	var annotation = ret[1];
	
	var unpair = null;
	if (type == 'minutia' && 'pairing' in annotation)
	{
		var mark;
		var print;
		if (view.getType() == 'mark')
		{
			mark = annotation;
			print = pianos.getView('print').getPairedMinutia(annotation.pairing);
			
		}
		else
		{
			mark = pianos.getView('mark').getPairedMinutia(annotation.pairing);
			
			print = annotation;
		}

		unpair = new Pianos4Action_PairMinutiae(mark, print, null);
	}
}

function Pianos4Action_GroupAnnotations(view, annotations, groupID)
{
	
	
	this.getName = function() {	return 'groupannotations' };
	this.getLabel = function() { return 'Group annotations' };
	
	this.redo = function()
	{
		for (var i in annotations)
		{
			var annot = annotations[i];
			annot.group = -1;
			view.updateAnnotation(annot, {group: groupID});
		}
	}

	this.undo = function()
	{
		for (var i in annotations)
		{
			var annot = annotations[i];
			if (annot.id in previous)
			{
				annot.group = -2;
				view.updateAnnotation(annot, {group: previous[annot.id]});
			}
			else
			{
				delete annot.group;
				view.updateAnnotation(annot, {});
			}
		}
	}
	
	var previous = {};
	for (var i in annotations)
	{
		var annot = annotations[i];
		if ('group' in annot)
			previous[annot.id] = annot.group;
	}
}


// ----------------------------
// Minutia-specific
// ----------------------------


/**
 * @constructor
 */
function Pianos4Action_PairMinutiae(mark, print, id)
{
	
	
	this.getName = function()
	{
		return 'pairminutiae';
	}
	
	this.getLabel = function()
	{
		return !mark||!print ? 'Unpair minutiae' : 'Pair minutiae';
	}
	
	this.redo = function()
	{
		begin();
		for (var i in ops)
			apply(ops[i], false);
		end();
	}

	this.undo = function()
	{
		begin();
		for (var i in ops)
			apply(ops[i], true);
		end();
	}

	var ops = [];
	var updates = null;
	
	var plan = function(minutia, isMark)
	{
		var previous = null;
		if ('pairing' in minutia)
		{
			previous = minutia.pairing;
			
			var otherViewName = isMark ? 'print' : 'mark';
			var otherView = pianos.getView(otherViewName);
			
			var paired = otherView.getPairedMinutia(minutia.pairing);
			
			
			ops.push({ view:otherView, minutia:paired, redo:null, undo:paired.pairing });
		}
		
		var viewName = isMark ? 'mark' : 'print';
		var view = pianos.getView(viewName);
		ops.push({ view:view, minutia:minutia, redo:id, undo:previous });
	}
	
	var begin = function()
	{
		
		updates = {};
	}
	
	var end = function()
	{
		for (var i in updates)
			updates[i][0].updateMinutiaPairing(updates[i][1]);
		updates = null;
	}
	
	var apply = function(op, isUndo)
	{
		var value = isUndo ? op.undo : op.redo;
		if (value !== null)	
			op.minutia.pairing = value;
		else
			delete op.minutia.pairing;
	
		var updateIndex = op.view.getType()+'_'+op.minutia.id;
		if (!(updateIndex in updates))
			updates[updateIndex] = [op.view,op.minutia];
	}
	
	if (mark && print && 'pairing' in mark && 'pairing' in print && mark.pairing == print.pairing)
	{
		// Re-pairing of already paired minutiae must be processed differently
		ops.push({ view:pianos.getView('mark'), minutia:mark, redo:id, undo:mark.pairing });
		ops.push({ view:pianos.getView('print'), minutia:print, redo:id, undo:print.pairing });
	}
	else
	{
		// All other cases use the standard algorithm
		if (mark)
			plan(mark, true);
		if (print)
			plan(print, false);
	}
}



// ----------------------------
// EditImage
// ----------------------------

/**
 * @constructor
 */
function Pianos4Action_EditImage(view, filters)
{
	

	var previous = view.getImageFilters();

	this.getName = function()
	{
		return 'editimage';
	}
	
	this.getLabel = function()
	{
		return 'Edit image';
	}
	
	this.redo = function()
	{
		view.setImageFilters(filters);
	}

	this.undo = function()
	{
		view.setImageFilters(previous);
	}
}

// ----------------------------
// Conclusions/observations
// ----------------------------

/**
 * @constructor
 */
function Pianos4Action_UpdateConclusions(previous, current, runFirstTime)
{
	

	var firstTime = typeof(runFirstTime) == 'boolean' ? !runFirstTime : true;

	this.getName = function()
	{
		return 'modconcl';
	}
	
	this.getLabel = function()
	{
		return 'Conclusions changed';
	}
	
	this.redo = function()
	{
		if (firstTime)
		{
			firstTime = false;
			return;
		}
		pianos.setConclusions(current);
	}

	this.undo = function()
	{
		pianos.setConclusions(previous);
	}
}

// ----------------------------
// SetData 
// ----------------------------

/**
 * @constructor
 */
function Pianos4Action_SetData(data, v3)
{
	this.getName = function() { return 'setdata'; }
	
	this.getLabel = function() { return data ? 'Load' : 'Clear' }
	
	this.redo = function()
	{
		if (v3)
			pianos.setDataV3(data);
		else
			pianos.setData(data);
	}

	this.undo = function()
	{
		pianos.setData(previous);
	}
	
	var previous = pianos.getData();
}
	

/* pianos4_annotation.js */
/*
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 Pianos4Annotation()
{
}
Pianos4Annotation._s_ids = 1;
Pianos4Annotation._s_pairIds = 1;


// ----------------------------
// Minutia
// ----------------------------

/**
 * @constructor
 */
function Pianos4Minutia(type, pos, angle)
{
	
	
	
	this.id = Pianos4Annotation._s_ids++;
	this.type = type;
	this.pos = pos;
	this.angle = angle;
}


// ----------------------------
// Ridge
// ----------------------------

/**
 * @constructor
 */
function Pianos4Ridge(points, color, width, opacity)
{
	var myID = null;
	if (!$.isArray(points))
	{
		var p = points; // points is in fact a javascript object, which contains only the data
						// and which we are converting into a Pianos4Ridge object
		
		
		myID = parseInt(p.id);
		points = p.points;
		color = p.color;
		width = p.width;
		opacity = p.opacity;
	}
	
	
	// if (color.length != 7)
		// var breakme = 0;
	
	
	

	this.id = myID ? myID : Pianos4Annotation._s_ids++;
	this.points = points;
	this.color = color;
	this.width = width;
	this.opacity = opacity;
}

// ----------------------------
// Area
// ----------------------------

/**
 * @constructor
 */
function Pianos4Area(points, color, type, value, holes)
{
	
	
	
	this.id = Pianos4Annotation._s_ids++;
	this.points = points;
	this.color = color;
	
	if (typeof(type) != 'undefined')
		this.type = type;
		
	if (typeof(value) != 'undefined')
		this.value = value;
		
	if (typeof(holes) != 'undefined')
		this.holes = holes;
}

Pianos4Area.getPolygonSurface = function(points)
{
	// Use the Surveyor's formula [1] to calculate the area of the polygon
	// [1]: http://www.maa.org/pubs/Calc_articles/ma063.pdf
	
	var p, q;
	var area = 0.0;
	var to = points.length;
	var last = points.length-1;
	for (var i=0;i<to;++i)
	{
		if (i == last)
		{	
			p = points[i];
			q = points[0];
		}
		else
		{
			p = points[i];
			q = points[i+1];
		}
		var det = p[0]*q[1] - q[0]*p[1];
		area += det;
	}
	// The sign of the result depends on the ordering of the vertices
	// (CCW or CW). Since we have no way of enforcing CCW (actually, 
	// CW since or Y axis is reversed - but anyway, we can't enforce one
	// or the other) we just take the absolute value of the result, which
	// is negative if CCW (should be negative if CW if the Y axis was up)
	// and positive if CW.
	return Math.abs(area);
}

Pianos4Area.getSurface = function(area)
{
	var hull = Pianos4Area.getPolygonSurface(area.points);
	var holes = 0.0;
	if ('holes' in area)
	{
		for (var i in area.holes)
			holes += Pianos4Area.getPolygonSurface(area.holes[i]);
	}
	
	return hull - holes;
}

// ----------------------------
// Comment
// ----------------------------

/**
 * @constructor
 */
function Pianos4Comment(content, color, pos, boxpos)
{
	
	
	this.id = Pianos4Annotation._s_ids++;
	this.content = content;
	this.color = color;
	this.pos = pos;
	this.boxpos = boxpos;
}


/* pianos4_common.js */
/*
PiAnoS (Picture Annotation System), A fingerprint examiner training tool
Copyright (C) 2011-2014 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/.
*/

// Handy sprintf() replacement. See http://stackoverflow.com/questions/610406/javascript-printf-string-format
// Usage: "{0} is much cooler than {1} ?".format("Python", "Javascript")
String.prototype.format = function() {
    var formatted = this;
    for (var i = 0; i < arguments.length; i++) {
        var regexp = new RegExp('\\{'+i+'\\}', 'gi');
        formatted = formatted.replace(regexp, arguments[i]);
    }
    return formatted;
};

merge = function(obj1, obj2)
{
	for (attrname in obj2) { obj1[attrname] = obj2[attrname]; }
	return obj1;
}

array_shuffle = function(a)
{
	var j = 0;
	var valI = '';
	var valJ = valI;
	var l = a.length - 1;
	while(l > -1)
	{
		j = Math.floor(Math.random() * l);
		valI = a[l];
		valJ = a[j];
		a[l] = valJ;
		a[j] = valI;
		l = l - 1;
	}
	return a;
}

array_chunk = function(input, size) 
{
	for (var x, i = 0, c = -1, l = input.length, n = []; i < l; i++) {
		(x = i % size) ? n[c][x] = input[i] : n[++c] = [input[i]];    }
 
	return n;
}

// Remove all occurrences of an element from an array. 
// Hoping that the next function I need to write here is not the '+' operator for integer numbers 
// EDIT: this, in contrast, already exists in standard Javascript. Pfeww, what a relief.
function array_remove(array, value)
{
	var idx = -1;
	do
	{
		idx = array.indexOf(value);
		if (idx >= 0)
			array.splice(idx, 1);
	} while (idx >= 0);
	return array;
}

/** @nosideeffects */
log = function(what)
{
	if (typeof console == 'undefined')
		return;
	if (console && console.log)
	{
		//console.trace('Logging stuff');
		console.log(what);
	}
}

/** @nosideeffects */
assert = function(cond)
{
	if (!cond)
	{
		if (console && console.trace)
			console.trace('Assertion failed');

		throw 'Assertion failed';
	}
}

clampAngle = function(angle)
{
	while (angle > 180.0)
		angle -= 360.0;
	while (angle < -180.0)
		angle += 360.0;
	return angle;
}

// jQuery 'plugin' function removeCSS()
// proposed by bluish, on http://stackoverflow.com/questions/955030/remove-css-from-a-div-using-jquery
// This function removes a specific value from the style attribute by parsing it into an array, removing
// the desired style and rebuilding it.
jQuery.fn.extend
({
    removeCSS: function(cssName) {
        
		return this.each(function() {
			
			if (!$(this).attr('style'))
				return $(this);
				
            return $(this).attr('style',

				jQuery.grep($(this).attr('style').split(";"),
						function(curCssName) {
							if (curCssName.toUpperCase().indexOf(cssName.toUpperCase() + ':') < 0)
								return curCssName;
						}).join(";"));
        });
    }
});

// jQuery 'plugin' function serializeObject()
// proposed by Tobias Cohen, on http://stackoverflow.com/questions/1184624/serialize-form-to-json-with-jquery
// Works like $.serializeArray but puts the results in an associative object. Handles duplicate (multiple fields
// with the same name name) by aggregating values into an array.
$.fn.serializeObject = function()
{
    var o = {};
    var a = this.serializeArray();
    $.each(a, function() {
        if (o[this.name] !== undefined) {
            if (!o[this.name].push) {
                o[this.name] = [o[this.name]];
            }
            o[this.name].push(this.value || '');
        } else {
            o[this.name] = this.value || '';
        }
    });
    return o;
};


createToolSlider = function(label, sliderConf)
{
	var setValueLabel = function(value) {
		var v = value;
		if ('unit' in sliderConf)
			v += ' '+sliderConf.unit;
		sliderOuter.find('span._value').text(v);
	};
	
	var conf = 
	{
		min: 1,
		max: 100,
		value: 50,
		slide: function(event,ui) {
			setValueLabel(ui.value);
			if (sliderConf && 'onSlide' in sliderConf)
				sliderConf.onSlide(ui.value);
		},
		outerClass: 'pianos4_tool_slider',
	};
	$.extend(conf, sliderConf);
	
	var sliderOuter = $('<div />').addClass(conf.outerClass);
	var slider = $('<div />');
	slider.slider(conf);
	var value = $('<span />');
	if ('unit' in conf)
		value.text(' '+conf.unit);
	sliderOuter
		.append($('<span class="_label" />').text(label))
		.append($('<span class="_value" />'))
		.append(slider)
	;
	setValueLabel(conf.value);
	return sliderOuter;
}

// Returns true if the specified SVG node has the specified CSS class.
// Firefox provides the handy 'classList' attribute, so this function does 
// not make much sense for it, but as other browsers lack it, it fills the gap.
// Note: this function will (voluntarily) fail for regular HTML DOM nodes that
// 		 do not have an SVGAnimatedString as a className (or anything that does
//       not have the baseVal thing.
var svgHasClass = function(node, myclass)
{
	if (node.classList)
	{
		return node.classList.contains(myclass);
	}
	else if (node.className)
	{
		return node.className.baseVal.split(' ').indexOf(myclass) >= 0;
	}
	else
		return false;
}


// Function to set the user selection to the provided (pure HTML) DOM element
// Credit to Tim Down 
// Source: http://stackoverflow.com/questions/4183401/can-you-set-and-or-change-the-users-text-selection-in-javascript
function selectElementContents(el) 
{
    if (window.getSelection && document.createRange) 
	{
        var sel = window.getSelection();
        var range = document.createRange();
        range.selectNodeContents(el);
        sel.removeAllRanges();
        sel.addRange(range);
    } else if (document.selection && document.body.createTextRange) 
	{
        var textRange = document.body.createTextRange();
        textRange.moveToElementText(el);
        textRange.select();
    }
}

// Converts a timestamptz from PostgreSQL into a Javascript Date object
// The JS parser (at least using Mozilla) is broken and does not allow
// ISO-8601 conforming dates like '2012-02-20 09:54:56.34+01' to be interpreted
// correctly. It needs a format like '2012-02-20T09:54:56.34+01:00' (notice the
// date/time separator 'T' and the to-the-minute time zone offset). I hate the
// JS ecosystem - again.
pgTimestamptzToDate = function(iso_time)
{
	// Try to decode the string directly - we never know
	var ts = Date.parse(iso_time);
	if (isNaN(ts))
	{
		var input = iso_time.replace(' ', 'T');
		var plus_or_minus = input.lastIndexOf('+');
		// If the timezone offset is negative, we're looking for a minus sign, not plus
		if (plus_or_minus == -1)
			plus_or_minus = input.lastIndexOf('-');
		var colon = input.lastIndexOf(':');
		if (plus_or_minus >= 0 && colon < plus_or_minus)
		{
			input += ':00';
		}
		ts = Date.parse(input);
		if (isNaN(ts))
		{
			// Okay, take the hammer out and try again. Not sure if this portion is required/used on something else than IE9.
			// Source: http://stackoverflow.com/questions/5802461/javascript-which-browsers-support-parsing-of-iso-8601-date-string-with-date-parse
			// Not sure if it's correct, though
			var reIsoDate = /^(\d{4})-(\d{2})-(\d{2})((T)(\d{2}):(\d{2})(:(\d{2})(\.\d*)?)?)?(Z$|\+\.+$)?/;
			var m = input.match(reIsoDate);
			if (m) 
				return new Date(Date.UTC(+m[1], +m[2] - 1, +m[3], +m[6] || 0, +m[7] || 0, +m[9] || 0, parseInt((+m[10]) * 1000) || 0));
			else
				throw 'Could not parse timestamp as ISO-8601: '+iso_time;
		}
	}
	return new Date(ts);
}

// Get the parameter named 'name' from the query string of the current URL.
// Source and credits: http://stackoverflow.com/questions/1403888/get-url-parameter-with-jquery
function getURLParameter(name) 
{
    return decodeURIComponent(
        (location.search.match(RegExp("[?|&]"+name+'=(.+?)(&|$)'))||[,null])[1]
    );  
}


/* pianos4_constants.js */
/*
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/.
*/

Pianos4Constants = {

	// BEGIN-JSON-PARSABLE-DATA
	
	quality: {
		ansi_nist: [
			{id: 1, color: '#00ffff', label: 'Definitive Pores'},
			{id: 2, color: '#3366ff', label: 'Definitive Ridge Edges'},
			{id: 3, color: '#01ff02', label: 'Definitive Minutiae'},
			{id: 4, color: '#ffff00', label: 'Debatable Minutiae'},
			{id: 5, color: '#fe0000', label: 'Debatable Ridge Flow'},
			{id: 6, color: '#010101', label: 'Background'},
		],
		standard: [
			{id: 1, color: '#2ca02c', label: 'High'},
			{id: 2, color: '#ff7f0e', label: 'Medium'},
			{id: 3, color: '#d62728', label: 'Low'},
		],
		conversions: {
			an_to_std: {
				1: 1, 2: 1,
				3: 2, 4: 2,
				5: 3, 6: 3,
			},
			std_to_an: {
				1: 1,
				2: 3,
				3: 5,
			}
		},
	},
	
	details: [
		{id: 1, color: '#e377c2', label: 'Crease'},
		{id: 2, color: '#17becf', label: 'Scar'},
		{id: 3, color: '#bcbd22', label: 'Other (incipients, level 3 detail, etc.)'},
	],
	
	conclusions: {
		analysis: {
			suitability_approach: [
				{id: 1, label: '#1 (value for identification)'},
				{id: 2, label: '#2 (value for comparison)'},
			],
			value: [
				{id: 1, label: 'Suitable for identification'},
				{id: 2, label: 'Suitable only for exclusion'},
				{id: 0, label: 'No value'}
			],
			degradation: [
				{id: 1,  label: 'Low contrast with background'},
				{id: 2,  label: 'Substrate noise, texture or pattern'},
				{id: 3,  label: 'Tonal (color) reversal'},
				{id: 4,  label: 'Double tap'},
				{id: 5,  label: 'Superimposed (multiple impressions overlaid)'},
				{id: 6,  label: 'Extreme deposition pressure'},
				{id: 7,  label: 'Slippage (smearing or drag marks)'},
				{id: 8,  label: 'Twisting'},
				{id: 9, label: 'Non-contiguous ridge detail (separated by a break in substrate or a break in the latent)'},
 			],
			details: {
				'Level 1': [
					{id: 100,  label: 'Distinct'},
					{id: 101,  label: 'Indistinct'},
				],
				'Level 2': [
					{id: 200,  label: 'Most distinct'},
					{id: 201,  label: 'Few distinct'},
					{id: 202,  label: 'Most indistinct'},
				],
				'Level 3': [
					{id: 300,  label: 'Abundant'},
					{id: 301,  label: 'Minimal'},
					{id: 302,  label: 'None'},
				],
			},
			general_pattern: [
				{id: 1, label: 'Simple arch', url: 'css/general_patterns/arch_h_2.jpg'},
				{id: 2, label: 'Tented arch', url: 'css/general_patterns/t_arch_h_2.jpg'},
				{id: 3, label: 'Right loop', url: 'css/general_patterns/r_loop_h_1.jpg'},
				{id: 4, label: 'Left loop', url: 'css/general_patterns/l_loop_h_1.jpg'},
				{id: 5, label: 'Whorl', url: 'css/general_patterns/whorl_h_1.jpg'},
			],
		},
		comparison: {
			decision: [
				{id: 0, label: 'Exclusion'},
				{id: 3, label: 'Leaning towards exclusion'},
				{id: 5, label: 'Inconclusive'},
				{id: 7, label: 'Leaning towards identification'},
				{id: 10, label: 'Identification'},
			],
			decision_step1: [
				{id: 1, label: 'Identification'},
				{id: 0, label: 'Exclusion'},
				{id: 2, label: 'Inconclusive'},
			],
			decision_step2: [
				{id: 1, label: 'Leaning towards identification'},
				{id: 2, label: 'Leaning towards exclusion'},
				{id: 0, label: 'Neither'},
			],
			details: {
				'Level 1': [
					{id: 100,  label: 'Yes'},
					{id: 101,  label: 'No'},
				],
				'Level 2': [
					{id: 200,  label: 'Yes'},
					{id: 201,  label: 'No'},
				],
				'Level 3': [
					{id: 300,  label: 'Yes'},
					{id: 301,  label: 'No'},
				],
			},
		},
	},
	
	acev: {
		
		steps: [
			{id: 1, label: 'Analysis'},
			{id: 2, label: 'Comparison'},
			{id: 3, label: 'Evaluation'},
			{id: 4, label: 'Verification'},
		],
		
		progress: [
			{id: 0, label: 'Exercise started'},
			{id: 1, label: 'Analysis started'},
			{id: 2, label: 'Analysis submitted'},
			{id: 3, label: 'Comparison started'},
			{id: 4, label: 'Comparison submitted'},
			{id: 5, label: 'Evaluation started'},
			{id: 6, label: 'Evaluation submitted'},
		],
	},
	
	// END-JSON-PARSABLE-DATA
	
	getConstantById: function(id, array)
	{
		for (var i in array)
		{
			var item = array[i];
			if (item.id == id)
				return item;
		}
		throw 'Could not find the id '+id+' in the list of constants';
	}

};

Pianos4Constants.getItemById = function(array, id)
{
	for (var i in array)
	{
		var item = array[i];
		if (item.id == id)
			return item;
	}
	throw 'Could not find the id '+id+' in the list of constants';
}


/* pianos4_help.js */
/*
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/.
*/

Pianos4Help = {

	compile: function(lines)
	{
		var ret = $('<div />');
		var re_buttons1 = new RegExp(/^(<<.*>>):(.*$)/);
		var re_buttons2 = new RegExp(/(<<(\w+)\:(\w+)>>)/);
		var re_link = new RegExp(/(\[\[(.+)\|(\w+)\]\])/);
		var re_extlink = new RegExp(/(\[\[(.+)\|(\^(.+))\]\])/);
		var re_fnsubst = new RegExp(/`(.+)`/);
		for (var i in lines) (function(line)
		{
			var m1 = re_buttons1.exec(line);
			if (m1 && m1.length==3 && m1[1] && m1[2])
			{
				var keys_src = m1[1];
				var keys = [];
				var m2 = re_buttons2.exec(keys_src);
				while (m2 && m2.length==4 && m2[1] && m2[2] && m2[3])
				{
					keys.push([m2[2],m2[3]]);
					keys_src = keys_src.slice(m2[1].length);
					m2 = re_buttons2.exec(keys_src);
				}
				var description = m1[2];

				var keysSpan = $('<span />').addClass('pianos4_help_keys');
				var numAdded = 0;
				for (var j in keys)
				{
					if (numAdded > 0)
						keysSpan.append(' + ');
						
					var key = keys[j];
					var cls = null;
					var text = '';
					if (key[0] == 'mouse')
					{
						if (key[1] == 'middle')	
							cls = 'pianos4_help_mouse_middle';
						else if (key[1] == 'right')
							cls = 'pianos4_help_mouse_right';
						else
							cls = 'pianos4_help_mouse_left';
						cls = 'pianos4_help_mouse '+cls;
					}
					else
					{
						cls = 'pianos4_help_key';
						text = key[1];
					}
					var span = $('<span />').addClass(cls).addClass('ui-corner-all');
					if (text)
						span.text(text);
					keysSpan.append(span);
					numAdded++;
				}
				var wrapper = $('<div />').addClass('pianos4_help_shortcut')
					.append(keysSpan)
					.append($('<span />').addClass('pianos4_help_description').text(description));
					
				ret.append(wrapper);
			}
			else
			{
				var m3 = re_link.exec(line);
				if (m3 && m3.length == 4 && m3[2] && m3[3])
				{
					var idx = line.indexOf(m3[1]);
					
					var part1 = line.slice(0,idx);
					var part2 = line.slice(idx+m3[1].length);
					ret.append($('<div />').addClass('pianos4_help_paragraph')
						.append($('<span />').text(part1))
						.append($('<a />').text(m3[2]).attr('href','#help/'+m3[3]).click(function(e) {
							e.preventDefault();
							pianos.showHelp(m3[3]);
							var me = $(this);
							$(this).parents('.pianos4_dialog').dialog('close');
						}))
						.append($('<span />').text(part2)));
				}
				else
				{
					var m4 = re_extlink.exec(line);
					if (m4 && m4.length == 5 && m4[2] && m4[4])
					{
						var idx = line.indexOf(m4[1]);
						
						var part1 = line.slice(0,idx);
						var part2 = line.slice(idx+m4[1].length);
						ret.append($('<div />').addClass('pianos4_help_paragraph')
							.append($('<span />').text(part1))
							.append($('<a />').text(m4[2]).attr('href',m4[4]).attr('target','_blank'))
							.append($('<span />').text(part2)));
					}
					else
					{
						var m5 = re_fnsubst.exec(line);
						if (m5 && m5.length == 2 && m5[1])
						{
							var jscode = m5[1];
							var res = eval(jscode);
							var idx = line.indexOf(m5[0]);
							
							var part1 = line.slice(0,idx);
							var part2 = line.slice(idx+m5[0].length);
							ret.append($('<div />').addClass('pianos4_help_paragraph').text(part1+res+part2));
						}
						else
						{
							ret.append($('<div />').addClass('pianos4_help_paragraph').text(line));
						}
					}
				}
			}
		})(lines[i]);
		return ret;
	},
	
	topics: {
		quality: ['Help on quality maps', [
			'To be written'
		]],
		general: ['PiAnoS Help', [
			'The following topics are available:',
			'[[Keyboard shortcuts|shortcuts]]',
			'[[About PiAnoS|about]]',
		]],
		shortcuts: ['Keyboard shortcuts', [
			'Undo/Redo :',
			'<<key:ctrl>><<key:Z>>: undo',
			'<<key:ctrl>><<key:Y>>: redo',
			'Windows :',
			'<<key:F>>: toggle fullscreen mode',
			'<<key:home>>: fit images on both views',
			'<<key:T>>: toggle the toolbox (left pane)',
			'<<key:B>>: toggle the top bar (top pane)',
			'<<key:C>>: toggle the conclusion bar (bottom pane)',
			'<<key:L>>: toggle the layers and properties (right pane)',
			'Tools :',
			'<<key:E>>: switch to the edit tool',
			'<<key:shift>>: temporarily switch to the edit tool',
			'<<key:Q>>: switch to the quality tool',
			'<<key:M>>: switch to the minutiae tool',
			'<<key:O>>: switch to the other features tool',
			'<<key:P>>: switch to the pairing tool',
			'<<key:space>>: temporarily switch to the pan tool',
		]],
		about:['About PiAnoS', [
			'PiAnoS, a picture annotation system',
			'Version `pianos.getVersion()`',
			'Copyright (c) 2011-2014 University of Lausanne, Switzerland',
			'-',
			'PiAnoS 4 was partially funded by the NIJ award 2010-DN-BX-K267:',
			'Improving the Understanding and the Reliability of the Concept of "Sufficiency" in Friction ridge Examination',
			'-',
			'Development: Thibault Genessay',
			'Advisors: Christophe Champod, Cedric Neumann, Glenn Langenburg',
			'-',
			'Credits:',
			'John Resig and the jQuery team (http://jquery.com)',
			'The jQuery UI team (http://jqueryui.com)',
			'Michael Bostock and the d3.js team (http://d3js.org)',
			'The PHP Group (http://www.php.net)',
			'The PostgreSQL Global Development Group (http://www.postgresql.org)',
			'The Apache Software Foundation (http://httpd.apache.org)',
			'Guido van Rossum and the Python Software Foundation (http://www.python.org)',
			'The StackOverflow community (http://stackoverflow.com)',
			'And certainly many more ...',
		]],
	},
	
	getGeneralHelp: function(topic)
	{
		if (topic in this.topics)
		{	
			var top = this.topics[topic];
			return [top[0], this.compile(top[1])];
		}
		
		return ['Help on unknown topic', this.compile(['We are sorry, but no help is available on \''+topic+'\'.'])];
	}
};


/* pianos4_report.js */
/*
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/.
*/


function Pianos4_Report(params)
{
	params = params || {};
	if (!params.p4) alert('Parameters to the PiAnoS 4 instance must be passed in');
	
	this.run = function()
	{
		
	
		showMainDialog();
		//ajaxUpload();
		//myAjaxDownload();
		//ajaxDownload();
	}
	
	var self = this;

	var makeDialogSection = function(type, title, description, image, preCallback)
	{
		var left = $('<div />');
		
		left.append($('<h3 />').css('margin','0 0 0.5em 0').text(title))
			.append($('<p />').css('margin-top','0.5em').text(description));
			
		var right = $('<div />').css('float','right').css('width','128px').css('height','128px').css('margin-left','0.5em')
			.css('background-color','yellow');
			
		
		var leftDown = false;

		var onDocMouseUp = function()
		{
			leftDown = false;
			$(document).unbind('mouseup', onDocMouseUp);
			
		}

		var section = $('<div />')
			.addClass('report_section')
			.addClass('ui-state-default ui-corner-all')
			.css('padding','1em')
			.css('cursor','pointer')
			.css('margin-bottom','1em')
			.hover(function() { // in
				$(this).addClass('ui-state-hover');
				if (leftDown)
					$(this).addClass('ui-state-active');
			}, function() { // out
				$(this).removeClass('ui-state-hover ui-state-active');
			})
			.on('mousedown', function() {
				$(this).addClass('ui-state-active');
				leftDown = true;
				$(document).bind('mouseup', onDocMouseUp);
			})
			.on('mouseup', function(e) {
				$(document).unbind('mouseup', onDocMouseUp);
				var activated = leftDown;
				leftDown = false;
				
				if (activated)
				{
					if (preCallback)
						preCallback();
					generateReport(type);
				}
				
			})
		;
		
		section
			.append(right)
			.append(left)
			.append($('<div />').css('clear','both'))
			.disableSelection();
			
		return section;
	}
	
	var showMainDialog = function()
	{
		var content = $('<div />');
	
		var dlg = $('<div />')
			.append(content);
			
		var closeDialogCB = function() {
			dlg.dialog('close');
		};
		
		content.append(makeDialogSection('analysis', 'Analysis', 'A report on the analysis phase with quite a lengthy explanation on how it works, what it does and what\'s the menu for today.', null, closeDialogCB));
		
		if (params.p4.phase != 'analysis')
		{
			content.append(makeDialogSection('comparison', 'Comparison', 'A report on the comparison phase. Obviously, this is only available when the comparison is at least started.', null, closeDialogCB));
		}
					
		pianos.dialog(dlg, {
			modal: true,
			title: 'Choose the report to generate',
			width: 500,
			close: function() { $(this).remove(); },
			buttons: {
				'Cancel': function() {
					$(this).dialog('close');
				},
			}
		});
	}
	
	var generateReport = function(type)
	{
		
		
		var serializeSVG = function(node)
		{
			if (typeof window.XMLSerializer != 'undefined')
				return (new window.XMLSerializer()).serializeToString(node);
			else if ('xml' in node)
				return node.xml;
			else
				throw 'Your crappy browser is not able to fetch the XML string from an SVG node. Please get a real computer.';
		}
		
		var src_data = [
			{ type: 'mark', nodesel: '.pianos4_mark svg' }
		];
		if (type == 'comparison')
			src_data.push(
			{ type: 'print', nodesel: '.pianos4_print svg' }
			);
			
		
		var data = {
			type: type,
			xml: {},
		};
		
		for (var i in src_data)
		{
			var item = src_data[i];
			var node = d3.select(item.nodesel).node();
			var xml = serializeSVG(node);
			data.xml[item.type] = xml;
		}
		
		
		ajaxUpload(data);
	}
	
	var ajaxDownload = function()
	{
		$.fileDownload(params.p4.reportToolURL);
	}
	
	var myAjaxDownload = function()
	{
		var iframe = $('<iframe />')
			//.css('display','none')
			.css('position','absolute')
			.css('top',0)
			.css('right',0)
			.css('width','300px')
			.css('height','200px')
			.css('z-index','2000')
			.addClass('ui-widget ui-widget-content')
			.attr('src', params.p4.reportToolURL)
			//.append($('<script />').attr('type','text/javascript').text("$(document).ready(function() { alert('toto'); });"))
			//.append('<body />')
		;
			
		iframe.appendTo($('body'));
	}
	
	var ajaxUpload = function(data)
	{
		pianos.ajax({
			url: params.p4.reportToolURL,
			data: JSON.stringify(data),
			type: 'post',
			processData: false,
			contentType: 'application/json',
			dataType: 'json',
			success: function(data)
			{
				if (data.error)
				{
					showErrorMessage('rpc', data.message);
				}
				else
				{
					
				}
			},
			error: function(xhr, status, error)
			{
				showErrorMessage('http', status, error);
			},
		});
	
	}

	var showErrorMessage = function(errtype, p1, p2)
	{
		
		
		var title = 'error';
		var content = $('<div />');
		var message = 'Unspecified error';
		
		if (errtype == 'http')
		{
			message = p1 != 'error' ? (p1 != p2 ? p1 + '\n' + p2 : p2) : p2;
			title = 'HTTP error';
		}
		else
		{
			message = p1;
			title = 'Unhandled exception';
		}
		
		content.append(
			$('<div />')	
				.text(message)
				.css('font-family', 'courier new')
				.css('white-space', 'pre-wrap')
				.css('word-wrap', 'break-word')
				.css('font-size', '0.75em')
				.css('margin','0.5em')
		).addClass('ui-state-error');
		
		var dlg = $('<div />')
			.append(content);
					
		pianos.dialog(dlg, {
			modal: true,
			title: title,
			width: 500,
			close: function() { $(this).remove(); },
			buttons: {
				'OK': function() {
					$(this).dialog('close');
				},
			}
		});
	}
}


/* pianos4_tool_arc.js */
/*
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 Pianos4ToolArc(params)
{
	// ------------------------------
	// Constructor
	// ------------------------------
	
	params = params || {};
	
	// ------------------------------
	// Public methods
	// ------------------------------
	
	this.getName = function()
	{
		return 'arc';
	}
	
	this.getLabel = function()
	{
		return 'Arc';
	}
	
	this.getIcon = function()
	{
		return 'icons/draw.png';
	}
	
	this.bind = function(aView)
	{
		
		view = aView;
	}
	
	this.unbind = function()
	{
		
		view = null;
	}
	
	this.activate = function()
	{
	}
	
	this.deactivate = function()
	{
	}
	
	this.onMouseDown = function(pos)
	{
		if (points.length == 0)
		{
			// The first click sets the starting point
			
			line = view.getSVG().append('svg:line')
				.style('stroke', color)
				.style('stroke-width', 3.0/view.getZoomFactor())
				.style('stroke-dasharray', 7.0/view.getZoomFactor()+' '+4.0/view.getZoomFactor())
				.attr('x1', pos[0])
				.attr('y1', pos[1])
				.attr('x2', pos[0])
				.attr('y2', pos[1])
			;
			points.push(pos);
			points.push(pos);
		}
		else if (points.length == 2)
		{
			
			line.remove();
			line = null;
			
			points.push(pos);

			path = view.getSVG().append('svg:path')
				.style('stroke', color)
				.style('stroke-width', 3.0/view.getZoomFactor())
				.style('fill','none')
			;
			
			updatePath();
		}
		else
		{
			
			path.remove();
			path = null;
			points = [];
			return true;
		}
	}
	
	this.onMouseMove = function(pos)
	{
		if (points.length == 2)
		{
			
			points[1] = pos;
			line.attr('x1', points[0][0])
				.attr('y1', points[0][1])
				.attr('x2', points[1][0])
				.attr('y2', points[1][1]);
		}
		else
		{
			
			points[2] = pos;
			updatePath();
		}
	}
	
	this.onMouseUp = function(pos)
	{
	}
	
	this.getOptionsPanel = function()
	{
		if (!options)
		{
			var panel = $('<div class="pianos4_arc_options" />');
		
			Pianos4ToolRidge.createColorPicker(panel, function() {
				if (this.title != color)
				{
					color = this.title;
					$(this).siblings('.pianos4_tool_color').removeCSS('border-color');
					$(this).css('border-color','#aaaaaa');
				}
			});
			
			var initialColor = panel.find('.pianos4_tool_color:first');
			color = initialColor.attr('title');
			initialColor.css('border-color','#aaaaaa');

			panel.append(createToolSlider('Width', {
				min:1,
				max:20,
				value:width,
				onSlide:function(value) { 
					width = value; 
				},
				unit: 'pt',
			}));
				
			panel.append(createToolSlider('Opacity', {
				min:1,
				max:100,
				value:100.0*opacity,
				onSlide:function(value) { 
					opacity = value/100.0; 
				},
				unit: '%',
			}));
			
			var randomID = 'pianos4_rid_'+(new Date().getTime());
			var cyclebox = $('<input type="checkbox" '+(cycle?'checked="checked"':'')+' id="'+randomID+'" />');
			cyclebox.bind('click', function() { cycle = this.checked; });
			var label = $('<label for="'+randomID+'" />').text('Cycle colors');
			
			panel.append($('<div />').append(cyclebox).append(label));

			options = panel;
		}
		
		return options;
	}
	
	this.cancel = function()
	{
		if (line)
			line.remove();
		line = null;
		if (path)
			path.remove();
		path = null;
		points = [];
	}
	
	// ------------------------------
	// Private variables
	// ------------------------------
	
	var self = this;
	var view = null;
	var path = null;
	var line = null;
	var points = [];
	var color = '#ff0000';
	var width = 5.0;
	var opacity = 0.85;
	var options = null;
	var cycle = true;
	
	// ------------------------------
	// Private functions
	// ------------------------------
	
	var updatePath = function()
	{
		
		var d23 = [points[2][0]-points[1][0], points[2][1]-points[1][1]];
		var radius = Math.sqrt(d23[0]*d23[0]+d23[1]*d23[1]);
		var def = 'M '+points[0][0]+','+points[0][1]+' a'+radius+','+radius+' 0 0,0 '+points[1][0]+','+points[1][1];
		path.attr('d', def);
	}
	
	var getDistance = function(pos0, pos1)
	{
		var p0 = view.getAbsPos(pos0);
		var p1 = view.getAbsPos(pos1);
		var v = [p1[0]-p0[0], p1[1]-p0[1]];
		var n = Math.sqrt(v[0]*v[0]+v[1]*v[1]);
		return n;
	}
}


/* pianos4_tool_brush.js */
/*
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 Pianos4ToolBrush(params)
{
	// ------------------------------
	// Constructor
	// ------------------------------
	
	params = params || {};
	
	
	// ------------------------------
	// Public methods
	// ------------------------------
	
	this.getName = function()
	{
		return 'brush';
	}
	
	this.getLabel = function()
	{
		return 'Brush';
	}
	
	this.getIcon = function()
	{
		return 'icons/tool_brush.png';
	}
	
	this.bind = function(aView)
	{
		
		view = aView;
	}
	
	this.unbind = function()
	{
		
		view = null;
	}
	
	this.activate = function()
	{
	}
	
	this.deactivate = function()
	{
	}
	
	this.onMouseDown = function(pos)
	{
		

		points.push(pos);
		
		var svgCanvas = view.dom().find('.pianos4_view_canvas');
		h5canvasOffset = svgCanvas.offset();
		h5canvas = $('<canvas />')
			.attr('width', svgCanvas.width())
			.attr('height', svgCanvas.height())
			.css('position','absolute')
			.css('top', h5canvasOffset.top+'px')
			.css('left', h5canvasOffset.left+'px')
			.css('z-index', 2000)
			.width(svgCanvas.width())
			.height(svgCanvas.height())
			.appendTo('body')
		;
		ctx = h5canvas.get(0).getContext('2d');
		
		view.getSVG().selectAll('image.pianos4_tool_brush_result').remove();
		
		if (prevData)
		{
			ctx.globalCompositeOperation = 'source-over';
			//ctx.globalCompositeOperation = 'destination-atop';
			ctx.putImageData(prevData, 0, 0);
		}
		
		//ctx.fillRect(10,10,100,100);
		ctx.lineCap = 'round';
		ctx.lineJoin = 'round';
		ctx.lineWidth = width;
		var rgba = [0,0,0,0];
		for (var i=0;i<3;++i)
			rgba[i] = parseInt(color.substr(1+2*i,2), 16);
		rgba[3] = opacity;
		
		ctx.strokeStyle = 'rgba('+rgba.join(',')+')';
		ctx.globalCompositeOperation = 'destination-atop';
		
		var p = unproject(pos);
		ctx.beginPath();
		ctx.moveTo(p[0], p[1]);
		ctx.lineTo(p[0], p[1]);
		ctx.stroke();
	}
	
	this.onMouseMove = function(pos)
	{
		

		points.push(pos);
		
		var p = unproject(pos);
		//ctx.fillCircle(p[0]-5,p[1],10);
		
		ctx.lineTo(p[0], p[1]);
		ctx.stroke();
	}
	
	this.onMouseUp = function(pos)
	{
		
		
		points = [];
		
		var data = ctx.getImageData(0, 0, h5canvas.attr('width'), h5canvas.attr('height'));
		
		prevData = data;
		
		view.getSVG().append('svg:image')
			.classed('pianos4_tool_brush_result', true)
			.attr('width', h5canvas.attr('width'))
			.attr('height', h5canvas.attr('height'))
			.attr('xlink:href', h5canvas.get(0).toDataURL());
		
		pianos.ajax({
			pendingMessage: 'Saving image',
			type: 'post',
			processData: false,
			contentType: 'application/json',
			url: 'http://localhost:8081/uflr?action=image&type=debug_save',
			data: JSON.stringify(h5canvas.get(0).toDataURL()),
			success: function(data) {
				if (!data.success)
				{
					alert('Error: '+data.message);
					return;
				}
				
				
			},
			error: function(xhr, status, error) {
				alert('HTTP error (code '+xhr.status+'): '+(error?error:'connection failed'));
			},
		});

		h5canvas.remove();
		h5canvas = null;
		h5canvasOffset = null;
		ctx = null;


		return true;
	}
	
	this.getOptionsPanel = function()
	{
		if (!options)
		{
			var panel = $('<div class="pianos4_brush_options" />');
		
			var selectColorCallback = function()
			{
				if ($(this).attr('data-color') != color)
				{
					color = $(this).attr('data-color');
					$(this).parents('._content').find('.pianos4_tool_color').removeCSS('border-color');
					$(this).css('border-color','#aaaaaa');
				}
			};
			
			Pianos4ToolRidge.createColorPicker(panel, selectColorCallback, 'standard');
			
			var initialColor;
			initialColor = panel.find('.pianos4_tool_color:first');
			color = initialColor.attr('data-color');
			initialColor.css('border-color','#aaaaaa');

			panel.append(createToolSlider('Width', {
				min:1,
				max:50,
				value:width,
				onSlide:function(value) { 
					width = value; 
				},
				unit: 'pt',
			}));
				
			panel.append(createToolSlider('Opacity', {
				min:1,
				max:100,
				value:100.0*opacity,
				onSlide:function(value) { 
					opacity = value/100.0; 
				},
				unit: '%',
			}));
			
			options = panel;
		}
		
		return options;
	}
	
	this.cancel = function()
	{
		points = [];
		if (h5canvas)
			h5canvas.remove();
		h5canvas = null;
		h5canvasOffset = null;
		ctx = null;
	}
	
	this.getHelp = function()
	{
		return Pianos4Help.compile([
				'Use the brush tool to do whatever a brush is meant to do.',
			]);
	}

	// ------------------------------
	// Private variables
	// ------------------------------
	
	var self = this;
	var view = null;
	var points = [];
	var color = '#ff0000';
	var width = 20.0;
	var opacity = 0.85;
	var options = null;
	var cycle = true;
	var h5canvas = null;
	var h5canvasOffset = null;
	var ctx = null;
	var prevData = null;
	
	// ------------------------------
	// Private functions
	// ------------------------------
	
	var unproject = function(pos)
	{
		
		var p = view.getAbsPos(pos);
		p[0] -= h5canvasOffset.left;
		p[1] -= h5canvasOffset.top;
		return p;
	}
}


/* pianos4_tool_comment.js */
/*
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 Pianos4ToolComment(params)
{
	// ------------------------------
	// Constructor
	// ------------------------------
	
	params = params || {};
	
	
	// ------------------------------
	// Public methods
	// ------------------------------
	
	this.getName = function()
	{
		return 'comment';
	}
	
	this.getLabel = function()
	{
		return 'Comment';
	}
	
	this.getIcon = function()
	{
		return 'icons/select_analysis.png';
	}
	
	this.bind = function(aView)
	{
		
		view = aView;
	}
	
	this.unbind = function()
	{
		
		view = null;
	}
	
	this.activate = function()
	{
	}
	
	this.deactivate = function()
	{
	}

	this.onMouseDown = function(pos)
	{
		if (!popup)
		{
			line = view.getSVG().append('svg:line')
				.attr('x1', pos[0])
				.attr('y1', pos[1])
				.attr('x2', pos[0])
				.attr('y2', pos[1])
				.style('stroke', color)
				.style('stroke-width', 2.0/view.getZoomFactor())
			;

			pianos.toggleLayer('comments', true);
		}
		else
			return validate();
	}
	
	this.onMouseMove = function(pos)
	{
		if (!popup)
		{
			line
				.attr('x2', pos[0])
				.attr('y2', pos[1])
			;
		}
		else
			return validate();
	}
	
	this.onMouseUp = function(pos)
	{
		if (!popup)
		{
			var content = '<p>Please enter your comment below</p><div><textarea rows="5" cols="30" /></div>';
			var dlg = $('<div />');
			dlg.html(content);
			
			popup = {
				dialog: dlg,
				validated: false,
			};
				
			pianos.dialog(dlg, {
				modal: true,
				title: 'New comment',
				buttons: {
					'OK': function() {
						popup.validated = true;
						$(this).dialog('close');
					},
					'Cancel': function() {
						$(this).dialog('close');
					}
				},
				position: view.getAbsPos(pos),
			});
		}
		else
			return validate();
	}
	
	this.cancel = function()
	{
		if (popup)
			return true;
			
		if (line)
			line.remove();
		line = null;
	}
	
	
	// ------------------------------
	// Private variables
	// ------------------------------
	
	var self = this;
	var view = null;
	var line = null;
	var popup = null;
	var color = '#e1c564';
	
	// ------------------------------
	// Private functions
	// ------------------------------
	
	var validate = function()
	{
		
		
		if (popup.dialog.dialog('isOpen'))
			return false;
		
		var pos0 = [parseFloat(line.attr('x1')), parseFloat(line.attr('y1'))];
		var pos1 = [parseFloat(line.attr('x2')), parseFloat(line.attr('y2'))];

		line.remove();
		line = null;
		
		if (popup.validated)
		{
			var content = popup.dialog.find('textarea').val();
			var action = new Pianos4Action_CreateAnnotation(view, 'comment', new Pianos4Comment(content, color, pos0, pos1));
			pianos.exec(action);
		}
		
		popup.dialog.remove();
		popup = null;
		return true;
	}
}


/* pianos4_tool_edit.js */
/*
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 Pianos4ToolEdit(params)
{
	// ------------------------------
	// Constructor
	// ------------------------------
	
	params = params || {};
	if (!params.lineColor) params.lineColor = '#dddd00';
	if (!params.fillColor) params.fillColor = '#101000';
	if (typeof(params.v3) == 'undefined') params.v3 = false;
	if (!params.hotkey) params.hotkey = 'ctrl';
	
	// ------------------------------
	// Public methods
	// ------------------------------
	
	this.getName = function()
	{
		return 'edit';
	}
	
	this.getLabel = function()
	{
		return 'Edit';
	}
	
	this.getIcon = function()
	{
		return 'icons/select.png';
	}
	
	this.bind = function(aView)
	{
		
		view = aView;
		//if (!(view in gizmos))
		//	gizmos[view] = {};
	}
	
	this.unbind = function()
	{
		
		view = null;
	}
	
	this.activate = function()
	{
		
	}
	
	this.deactivate = function()
	{
		
	}
	
	this.onMouseDown = function(pos, target, event)
	{
		if (popup)
			return false;
			
		//
		var picked = view.pickByNode(target);
		if (!picked)
		{
			
			dragSel = {
				from: pos,
				to: pos,
				quad: view.getSVG().append('svg:rect')
						.attr('x', pos[0])
						.attr('y', pos[1])
						.attr('width', 0)
						.attr('height', 0)
						.style('stroke', params.lineColor)
						.style('stroke-width', 1.0/view.getZoomFactor())
						.style('fill', params.lineColor)
						.style('fill-opacity', 0.3)
			}
		}
		else
		{
			
			
			var movingOnFirstClick = false;
			if (!view.isSelected(picked.annotation))
			{
				view.selectAnnotation(picked.annotation, !event.ctrlKey);
				// Since we will not "see" the gizmo in the DOM (as the picking already occured before
				// the selection, no action will be triggered. This flag works around this by forcing
				// a move, which is quite natural and allows one to select-and-drag an object that was
				// initially not selected. Note that minutiae cannot be rotated and ridge/area points
				// moved this way - but again, the user will find the behaviour quite sensible.
				movingOnFirstClick = true;
			}
				
			manipulated = { obj: picked, pos: pos, action: null };

			if (picked.type == 'minutia')
			{
				if (svgHasClass(picked.originalNode, '_gizmo') || movingOnFirstClick)
				{
					
					manipulated.action = 'move';
					manipulated.transform = getSVGTransform(picked.node, true);
					manipulated.origPos = [manipulated.transform.matrix.e, manipulated.transform.matrix.f];
				}
				else if (svgHasClass(picked.originalNode, '_rot'))
				{
					
					manipulated.action = 'rotate';
					manipulated.transform = getSVGTransform(picked.node, false);
				}
			}
			else if (picked.type == 'ridge' || picked.type == 'area')
			{
				if (svgHasClass(picked.originalNode, '_handle'))
				{
					// Moving a ridge/area handle
					manipulated.action = 'move';
					var gizmo = d3.select(picked.node).select('path._gizmo').node();
					manipulated.pointID = parseInt(picked.originalNode.id.split('_')[2]);
					
					manipulated.pathPoint = gizmo.pathSegList.getItem(manipulated.pointID);
					manipulated.handle = d3.select(picked.originalNode);
					manipulated.origPos = [manipulated.pathPoint.x, manipulated.pathPoint.y];
					
				}
				else
				{
					// Moving the ridge/area itself (most likely to make sense only for areas but who knows)
					
					manipulated.action = 'move';
					// Apply a transform to the node while we drag it - we'll remove it on mouse release
					d3.select(picked.node).attr('transform','translate(0,0)');
					manipulated.transform = getSVGTransform(picked.node, true);
					manipulated.origPos = [manipulated.transform.matrix.e, manipulated.transform.matrix.f];
				}
			}
		}
	}
	
	this.onMouseMove = function(pos, target)
	{
		if (popup)
			return false;
			
		if (dragSel)
		{
			dragSel.to = pos;
			var p = $.extend(true,[],dragSel.from);
			var size = [pos[0]-p[0], pos[1]-p[1]];
			for (var i=0;i<2;++i) {
				if (size[i] < 0.0) {
					p[i] += size[i];
					size[i] = Math.abs(size[i]);
				}
			} 
			dragSel.quad.attr('x', p[0]).attr('y', p[1]).attr('width', size[0]).attr('height', size[1]);
			dragSel.tl = p;
			dragSel.br = [p[0]+size[0],p[1]+size[1]];
		}
		else if (manipulated && manipulated.action)
		{
			if (manipulated.action == 'move')
			{
				manipulated.offset = [pos[0]-manipulated.pos[0], pos[1]-manipulated.pos[1]];
				var newPos = [manipulated.origPos[0]+manipulated.offset[0], manipulated.origPos[1]+manipulated.offset[1]];
				
				if ('transform' in manipulated)
				{
					manipulated.transform.setTranslate(newPos[0], newPos[1]);
				}
				else if ('pathPoint' in manipulated)
				{
					manipulated.pathPoint.x = newPos[0];
					manipulated.pathPoint.y = newPos[1];
					manipulated.handle.attr('cx', newPos[0])
										.attr('cy', newPos[1]);
				}
				else
				{
					
				}
			}
			else if (manipulated.action == 'rotate')
			{
				var offset = [pos[0]-manipulated.obj.annotation.pos[0], pos[1]-manipulated.obj.annotation.pos[1]];
				var l = Math.sqrt(offset[0]*offset[0]+offset[1]*offset[1]);
				var o = [offset[0]/l, offset[1]/l];
				var acos = Math.acos(o[0]);
				var anglerad = Math.asin(o[1]) < 0 ? -acos : acos;
				var angle = anglerad/Math.PI*180.0;
				manipulated.transform.setRotate(angle, 0, 0);
				manipulated.angle = angle;
			}
		}
	}
	
	this.onMouseUp = function(pos, target, event)
	{
		if (!popup)
		{
			if (dragSel)
			{
				dragSel.quad.remove();
				
				view.selectAnnotation(null, true);

				if ('tl' in dragSel)
				{
					var sel = [];
					var minutiae = view.getData('minutiae').minutiae;
					for (id in minutiae)
					{
						var min = minutiae[id];
						if (min.pos[0] >= dragSel.tl[0] && min.pos[0] <= dragSel.br[0]
						&& min.pos[1] >= dragSel.tl[1] && min.pos[1] <= dragSel.br[1])
							sel.push(min);
					}
					view.selectAnnotation(sel, false);
				}
				
				dragSel = null;
			}
			else if (manipulated)
			{
				if (event.button == 2)
				{
					var picked = view.pickByNode(target);
					if (picked && picked.type == manipulated.obj.type && picked.id == manipulated.obj.id)
					{
						
						showPopup(pos);
						return false;
					}
				}
				else if (manipulated.action)
				{
					if (manipulated.action == 'move' && 'offset' in manipulated)
					{
						var a = manipulated.obj.annotation;
						var o = manipulated.offset;
						
						var mods = {};
						var label = null;
						if (manipulated.obj.type == 'minutia')
						{
							mods = { pos: [a.pos[0]+o[0], a.pos[1]+o[1]] };
							label = 'Move minutia';
						}
						else if (manipulated.obj.type == 'ridge' || manipulated.obj.type == 'area')
						{
							// We make a deep copy of the vector of points so that undo/redo acts on different arrays
							var newPoints = $.extend(true, [], a.points);
							if ('pointID' in manipulated)
							{
								// Moving a single point
								label = 'Move '+manipulated.obj.type+' point';
								
								newPoints[manipulated.pointID][0] += o[0];
								newPoints[manipulated.pointID][1] += o[1];
							}
							else
							{
								// Moving the complete ridge/area is akin to moving all the points identically
								label = 'Move '+manipulated.obj.type;
								for (var i=0;i<newPoints.length;++i)
								{
									newPoints[i][0] += o[0];
									newPoints[i][1] += o[1];
								}
								if ('holes' in a)
								{
									var newHoles = $.extend(true, [], a.holes);
									for (var h in newHoles)
									{
										var hole = newHoles[h];
										for (var i=0;i<hole.length;++i)
										{
											hole[i][0] += o[0];
											hole[i][1] += o[1];
										}
									}
									mods.holes = newHoles;
								}
								// Don't forget to remove the transform attribute
								d3.select(manipulated.obj.node).attr('transform',null);
							}
							mods.points = newPoints;
						}
						
						var action = new Pianos4Action_ModifyAnnotation(view, a, mods);
						if (label)
							action.setLabel(label);
						pianos.exec(action);
					}
					else if (manipulated.action == 'rotate' && manipulated.angle)
					{
						
						var action = new Pianos4Action_ModifyAnnotation(view, manipulated.obj.annotation, { angle: manipulated.angle })
						action.setLabel('Rotate minutia');
						pianos.exec(action);
					}
					else if (manipulated.action == 'move' && !('offset' in manipulated) && (manipulated.obj.type == 'ridge' || manipulated.obj.type == 'area') && !('pointID' in manipulated))
					{
						// Corner case to remove the transform attribute for no-op ridge/area dragging
						d3.select(manipulated.obj.node).attr('transform',null);
					}
				}

				manipulated = null;
			}
			return true;
		}
		else
		{
			
	
			hidePopup();
			
			if (popup.choice)
			{
				
				
				switch (popup.choice)
				{
				case 'delete':
					view.deleteSelection();
					break;
					
				case 'rempair': 
					(function() {
						
						var markMin = null;
						var printMin = null;
						if (view.isMark())
							markMin = manipulated.obj.annotation;
						else
							printMin = manipulated.obj.annotation;
						var action = new Pianos4Action_PairMinutiae(markMin, printMin, null);
						pianos.exec(action);					
						pianos.onSelectionChanged(view);
					})(); 
					break;
				
				case 'group':
					(function() {
						var sel = view.getSelection();
						var annots = [];
						for (var i in sel)
							annots.push(view.getAnnotationById(i)[1]);
						var action = new Pianos4Action_GroupAnnotations(view, annots, 1);
						pianos.exec(action);					
					})(); 
					break;
					
				case 'bifurcation':
				case 'ridge_ending':
				case 'center':
				case 'unknown':
				case 'unknown2':
				case 'difference':
					var action = new Pianos4Action_ModifyAnnotation(view, manipulated.obj.annotation, {type: popup.choice});
					action.setLabel('Change minutia type');
					pianos.exec(action);
					break;
					
				case 'addpoint':
					
					var segID = getSegmentAt(manipulated.obj.annotation, manipulated.pos);
					segID = Math.max(segID, 0);
					
					var points = manipulated.obj.annotation.points;
					var newPoints = points.slice(0, segID+1).concat([manipulated.pos]).concat(points.slice(segID+1));
					var action = new Pianos4Action_ModifyAnnotation(view, manipulated.obj.annotation, { points: newPoints });
					action.setLabel('Add point to '+manipulated.obj.type);
					pianos.exec(action);
					break;
					
				case 'delpoint':
					var points = manipulated.obj.annotation.points;
					
					var newPoints = points.slice(0, manipulated.pointID).concat(points.slice(manipulated.pointID+1));
					var action = new Pianos4Action_ModifyAnnotation(view, manipulated.obj.annotation, { points: newPoints });
					action.setLabel('Remove '+manipulated.obj.type+' point');
					pianos.exec(action);
					break;
				}
				
			}
			
			popup = null;
			manipulated = null;
			
			if (event.button == 2)
				pianos.skipNextContextMenu();

			return true;
		}
	}
	
	this.cancel = function()
	{
		// if (line) 
			// line.remove();
		// line = null;
		if (popup)
			hidePopup();
		popup = null;
	}

	this.getOptionsPanel = function()
	{
		if (!options)
		{
			var panel = $('<div class="pianos4_edit_options" />');
			
			panel.append($('<div class="_hint" />').text('Use ').append($('<strong />').text('Ctrl+Click')).append(' to select multiple annotations'));
			
			options = panel;
		}
		
		return options;
	}
	
	this.getHelp = function()
	{
		return Pianos4Help.compile([
			'Use the edit tool to select and modify annotations.',
			'<<mouse:left>>: select an annotation',
			'<<key:ctrl>><<mouse:left>>: add to the selection',
			'<<key:shift>>: (hold) temporarily switch to the edit tool',
			'<<key:E>>: (shortcut) switch to the edit tool',
		]);
	}
	
	// ------------------------------
	// Private variables
	// ------------------------------
	
	var self = this;
	var view = null;
	var manipulated = null;
	var dragSel = null;
	var popup = null;
	var options = null;
	
	// ------------------------------
	// Private functions
	// ------------------------------
	
	var showPopup = function(pos)
	{
		
		
		var docPos = view.getAbsPos(pos);
		popup = {
			overlay: $('<div class="ui-widget-overlay" style="width:'+$(document).width()+'px;height:'+$(document).height()+'px;z-index:1000;" />'),
			menu: $('<div class="ui-widget ui-widget-content pianos4_canvas_popup" style="z-index:1001;"></div>'),
			choice: null,
		};

		var items = {
			'Annotation': {
				'delete': 'Delete',
				'rempair': 'Remove pairing',
				'group': 'Group selected',
			},
		};
		var disabled = {};
		
		if (manipulated.obj.type == 'minutia')
		{
			if (params.v3)
			{
				items['Minutia type'] = {
					'bifurcation': 'Bifurcation',
					'ridge_ending': 'Ridge ending',
					'unknown': 'Unknown',
					'center': 'Center',
				};
			}
			else
			{
				items['Minutia type'] = {
					'bifurcation': 'Bifurcation',
					'ridge_ending': 'Ridge ending',
					'unknown': 'Type unknown',
					'unknown2': 'Position unknown',
					'difference': 'Difference',
					'center': 'Center',
				};
			}
		}
		else if (manipulated.obj.type == 'ridge' || manipulated.obj.type == 'area')
		{
			items['Edit shape'] = {
				'addpoint': 'Add point here',
				'delpoint': 'Remove this point',
			};
			
			if ('pointID' in manipulated)
			{
				disabled.addpoint = true;
				var minPoints = manipulated.obj.type == 'ridge' ? 2 : 3;
				if (manipulated.obj.annotation.points.length <= minPoints)
					disabled.delpoint = true;
			}
			else
				disabled.delpoint = true;
				
		}

		if (view.isMark())
			disabled.center = true;
		if (!('pairing' in manipulated.obj.annotation))
			disabled.rempair = true;
		
		if (view.getSelectionSize() <= 1)
			disabled.group = true;
			
		var ul = $('<ul />');
		for (var section in items)
		{
			ul.append($('<li />').text(section).addClass('ui-widget-header'));
			
			for (var title in items[section])
			{
				var item = items[section][title];
				var li = $('<li />').text(item)
					.attr('title',title);
				if (title in disabled)
				{
					li.addClass('ui-state-disabled');
				}
				else
				{
					li
						.addClass('ui-state-default')
						.bind('mouseenter', function() { $(this).removeClass('ui-state-default').addClass('ui-state-active');  popup.choice = this.title; })
						.bind('mouseleave', function() { $(this).removeClass('ui-state-active').addClass('ui-state-default');  popup.choice = null; })
					;
				}
				ul.append(li);
			}
		}
		popup.menu.append(ul);
		
		popup.menu.css('visibility','none');
		$('body').append(popup.overlay).append(popup.menu);

		var docSize = [$(window).width(), $(window).height()];
		var menuPos = [
			Math.max(0, Math.min(docSize[0]-popup.menu.outerWidth(), docPos[0])),
			Math.max(0, Math.min(docSize[1]-popup.menu.outerHeight(), docPos[1]))
		];
		popup.menu.css('left', menuPos[0]+'px')
					.css('top', menuPos[1]+'px')
					.removeCSS('visibility');
					
		pianos.enterModal();
	}
	
	var hidePopup = function()
	{
		popup.overlay.remove();
		popup.menu.remove();
		pianos.leaveModal();
	}
	
	var getSVGTransform = function(node, isTranslate)
	{
		var type = isTranslate ? SVGTransform.SVG_TRANSFORM_TRANSLATE : SVGTransform.SVG_TRANSFORM_ROTATE;
		for (var i=0;i<node.transform.baseVal.numberOfItems;++i)
		{
			var tr = node.transform.baseVal.getItem(i);
			if (tr.type == type)
				return tr;
		}
		
	}
	
	var getSegmentAt = function(annotation, point)
	{
		var compat = [];

		for (var i=1;i<annotation.points.length;++i)
		{
			var a = annotation.points[i-1];
			var b = annotation.points[i];
			
			var u = [b[0]-a[0], b[1]-a[1]];
			var lu = Math.sqrt(u[0]*u[0]+u[1]*u[1]);
			var un = [u[0]/lu, u[1]/lu];
			
			var v = [point[0]-a[0], point[1]-a[1]];
			var lv = Math.sqrt(v[0]*v[0]+v[1]*v[1]);
			var vn = [v[0]/lv, v[1]/lv];
			
			var cost = un[0]*vn[0]+un[1]*vn[1];
			
			var proj = cost*lv;
			
			if (proj >= 0 && proj < lu)
			{
				var proj2 = Math.sqrt(lv*lv-proj*proj);
				compat.push({i:i-1,p:proj2});
			}
		}
		
		if (compat.length)
		{
			compat.sort(function(a,b) { return a.p-b.p; });
			return compat[0].i;
		}
		
		return -1;
	}		


}


/* pianos4_tool_group.js */
/*
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 Pianos4ToolGroup(params)
{
	// ------------------------------
	// Constructor
	// ------------------------------
	
	params = params || {};
	
	// ------------------------------
	// Public methods
	// ------------------------------
	
	this.getName = function()
	{
		return 'group';
	}
	
	this.getLabel = function()
	{
		return 'Group';
	}
	
	this.getIcon = function()
	{
		return 'icons/tool_group.png';
	}
	
	this.bind = function(aView)
	{
		
		view = aView;
	}
	
	this.unbind = function()
	{
		
		view = null;
	}
	
	this.activate = function()
	{
		init();
		
	}
	
	this.deactivate = function()
	{
		cleanup();
		
	}
	
	this.onMouseDown = function(pos, target)
	{
		
		var picked = view.pickByNode(target);
		if (picked && picked.type == 'minutia')
		{
			start_add(picked);
		}
	}
	
	this.onMouseMove = function(pos, target)
	{
	}
	
	this.onMouseUp = function(pos, target)
	{
		end_add();
		
		
		
		return true;
	}
	
	this.getOptionsPanel = function()
	{
		if (!options)
		{
			var panel = $('<div class="pianos4_group_options" />');
		
			var randomID = 'pianos4_rid_pgo_'+(new Date().getTime());
			var opModeSelector = $('<select id="'+randomID+'" />');
			opModeSelector
				.append($('<option value="0" />').text('Combined'))
				.append($('<option value="1" />').text('Targets'))
				.css('margin-bottom', '3px')
				.change(function() {
					setOperationMode(parseInt($(this).val())>0);
				})
			;
			var initSel = '' + opMode;
			opModeSelector.find('option[value='+initSel+']').attr('selected','selected');
			panel.append($('<div />').append(opModeSelector));			
			
			options = panel;
		}
		
		return options;
	}

	this.getHelp = function()
	{
		return Pianos4Help.compile([
			'Use the group tool to associate minutiae on an image, so as to form either combined minutiae or target groups, depending on the operation mode of the tool.',
			'When operating in "combined minutia" mode, the ',
			'When the \'Eraser mode\' is active, a click will remove the pairing of the minutia pointed to - note that the number disappears from both images. You can also remove all pairing using the appropriate option.',
			'Since you cannot choose the pairing number, you may end up with non consecutive numbers and/or a sequence that does not start at 1. Using the \'Restart numbering\' option will re-assign sequential pairing numbers. You generally want to use this option as a final step to make the result look better.',
			'<<mouse:left>>: select a minutia',
			'<<mouse:left>>: (in eraser mode) remove grouping',
			'<<key:G>>: (shortcut) switch to the group tool',
			'Note: to see whether a minutia is part of a combined minutia, select it (using the select tool) and look at the Properties window. Target groups are shown on the image with a dedicated representation.',
		]);
	}
	
	// ------------------------------
	// Private variables
	// ------------------------------
	
	var self = this;
	var view = null;
	var options = null;
	var opMode = 0;
	var popups = null;
	var minutiae = null;
	
	
	// ------------------------------
	// Private functions
	// ------------------------------
	
	var setOperationMode = function(value)
	{
		if (opMode == value)
			return;
			
		opMode = value;
	}
	
	var init = function()
	{
		
		
		popups = [];
		
		w = $('<div class="pianos4_pairing_hint ui-widget ui-widget-content" />');
		
		var views = pianos.getViews();
		for (var i in views)
		{
			var canvas = views[i].getDOM();
			var p = canvas.offset();
			p.left += parseInt(canvas.css('border-left-width'));
			var node = w.clone();
			node.css('left',p.left).css('top',p.top).outerWidth(canvas.width())
				.append('<span class="_text">Pick a minutia to start grouping</span>')
			;
			$('body').append(node);
			popups.push(node);
		}
		
		minutiae = [];		
	}
	
	var cleanup = function()
	{
		
		for (var i in popups)
			popups[i].remove();
		popups = null;
		minutiae = null;
	}
	
	var start_add = function(minutia)
	{
		
		
		
		minutiae.push(minutia);
		
		if (popups.length != 1)
		{
			var to_keep_index = view.isMark() ? 0 : 1;
			popups[1-to_keep_index].remove();
			popups = [popups[to_keep_index]];
		}
		popups[0].addClass('ui-state-highlight').find('._text').text('Release the button');
		var g = view.getSVG().select('#minutia_'+minutia.id);
		
		g.attr('stroke-opacity',0.3);
	}
	
	var end_add = function()
	{
		
		var span = popups[0].removeClass('ui-state-highlight').find('._text');
		span.text('Pick a minutia to continue grouping ('+minutiae.length+' items in group)');
		if (minutiae.length >= 2)
		{
			span.append('&nbsp;&nbsp;').append($('<a />').attr('href','#').text('Done').click(function(e) {
				e.preventDefault();
				fsdsdffinalize();
			}));
		}
	}
}


/* pianos4_tool_minutia.js */
/*
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 Pianos4ToolMinutia(params)
{
	// ------------------------------
	// Constructor
	// ------------------------------
	
	params = params || {};
	if (!params.lineColor) params.lineColor = '#ff0000';
	if (typeof(params.v3) == 'undefined') params.v3 = false;
	if (typeof(params.autoPairing) == 'undefined') params.autoPairing = true;
	if (!params.phase) params.phase = 'comparison';
	if (typeof(params.singleClickDeselects) == 'undefined') params.singleClickDeselects = false;
	if (typeof params.markCanHaveCenter == 'undefined') params.markCanHaveCenter = false;
	
	
	
	// ------------------------------
	// Public methods
	// ------------------------------
	
	this.getName = function()
	{
		return 'minutia';
	}
	
	this.getLabel = function()
	{
		return 'Minutia';
	}
	
	this.getIcon = function()
	{
		return 'icons/tool_minutia.png';
	}
	
	this.bind = function(aView)
	{
		
		view = aView;
	}
	
	this.unbind = function()
	{
		
		view = null;
	}
	
	this.activate = function()
	{
	}
	
	this.deactivate = function()
	{
	}
	
	this.onMouseDown = function(pos)
	{
		if (!popup)
		{
			line = view.getSVG().append('svg:line')
				.attr('x1', pos[0])
				.attr('y1', pos[1])
				.attr('x2', pos[0])
				.attr('y2', pos[1])
				.style('stroke', params.lineColor)
				.style('stroke-width', 1.0/view.getZoomFactor())
			;
			
			if (autoPair && !autoPairInfo)
				autoPairInfo = {view:view,first:null};
				
			pianos.toggleLayer('minutiae', true);
		}
	}
	
	this.onMouseMove = function(pos)
	{
		if (!popup)
		{
			line
				.attr('x2', pos[0])
				.attr('y2', pos[1])
			;
		}
	}
	
	this.onMouseUp = function(pos, target, event)
	{
		var pos0 = [parseFloat(line.attr('x1')), parseFloat(line.attr('y1'))];
		var pos1 = [parseFloat(line.attr('x2')), parseFloat(line.attr('y2'))];
		
		if (popup)
		{

			line.remove();
			line = null;
			
			hidePopup();
			
			if (popup.choice)
			{
				var v = [pos1[0]-pos0[0], pos1[1]-pos0[1]];
				var n = Math.sqrt(v[0]*v[0]+v[1]*v[1]);
				var vn = [v[0]/n,v[1]/n];
				var angle = Math.acos(vn[0]);
				if (vn[1]<0) angle = -angle;
				var degrees = 180.0*angle/Math.PI;
				
				var minutia = new Pianos4Minutia(popup.choice, pos0, degrees);
				var action;
				
				
				if (autoPair && autoPairInfo.view.getType() != view.getType())
				{
					// Auto-pairing is ON and this minutia was created in another view than the
					// previous one: pair them together
					
					
					// Use a batch action that will both create the minutia and do the pairing
					action = new Pianos4Action_Batch();
					action.append(new Pianos4Action_CreateAnnotation(view, 'minutia', minutia));
					var markMin, printMin;
					if (view.isPrint())
					{
						markMin = autoPairInfo.first;
						printMin = minutia;
					}
					else
					{
						markMin = minutia;
						printMin = autoPairInfo.first;
					}
					action.append(new Pianos4Action_PairMinutiae(markMin, printMin, Pianos4Annotation._s_pairIds++));
					
					// Reset pairing for the next use.
					autoPairInfo = null;
				}
				else
				{
					action = new Pianos4Action_CreateAnnotation(view, 'minutia', minutia);
					
					if (autoPair)
					{
						// Auto-pairing is ON but either:
						// - this is the first minutia created -> memorize it for the future pairing
						// - this minutia was created in the same view than the previous one -> memorize
						//   it as well, so that pairing will now occur between this minutia and the next one.
						autoPairInfo.first = minutia;
					}
				}
				
				pianos.exec(action);
			}

			if (event.button == 2)
				pianos.skipNextContextMenu();

			popup = null;
			return true;
		}
		else
		{
			// Check if the user has made a click or a click-drag-release, using a fixed
			// tolerance (so that minimal drags are considered as clicks)
			var p0 = view.getAbsPos(pos0);
			var p1 = view.getAbsPos(pos1);
			var v = [p1[0]-p0[0], p1[1]-p0[1]];
			var n = Math.sqrt(v[0]*v[0]+v[1]*v[1]);
			if (n < 2)
			{
				
				line.remove();
				line = null;
				if (params.singleClickDeselects)
					view.selectAnnotation([], true);
				return true;
			}
			else
			{
				// OK, it was a real drag: show the minutia creation popup
				var docPos = view.getAbsPos(pos);
				showPopup(docPos);
			}
		}
	}
	
	this.cancel = function()
	{
		if (line) 
			line.remove();
		line = null;
		if (popup)
			hidePopup();
		popup = null;
	}
	
	this.getOptionsPanel = function()
	{
		if (!options)
		{
			var panel = $('<div class="pianos4_minutia_options" />');
		
			if (params.autoPairing)
			{
				var randomID = 'pianos4_rid_'+(new Date().getTime());
				var autoPairBox = $('<input type="checkbox" '+(autoPair?'checked="checked"':'')+' id="'+randomID+'" />');
				autoPairBox.bind('click', function() { 
					autoPair = this.checked; 
					pianos.toggleLayer('pairing', autoPair);
				});
				var label = $('<label for="'+randomID+'" />').text('Automatic pairing');
				
				panel.append($('<div />').append(autoPairBox).append(label));
			}
			
			var sliderValues = [0.25, 0.5, 0.75, 1, 1.5, 2, 3];
			panel.append(createToolSlider('Marker size', {
				min:1,
				max:sliderValues.length,
				value:markerSize,
				onSlide:function(value) {
					markerSize = value;
					
					
					pianos.setMinutiaMarkerSize(sliderValues[markerSize-1]);
				},
				unit: '',
			}));

			options = panel;
		}
		
		return options;
	}
	
	this.getHelp = function()
	{
		return Pianos4Help.compile([
			'Use the minutia tool to draw minutiae. To place a minutia, click the desired position and, keeping the button depressed, drag along the direction (a red line appears). Release when the direction is OK - the length of the drag does not matter. Finally, choose the appropriate minutia type from the popup menu.',
			'After a minutia has been placed, it can be moved and rotated using the edit tool - or by simply clicking it with this tool. When a minutia is selected (green overlay) the mouse cursor turns into a cross if inside the overlay, and into a rotation symbol if nearby. Click and drag to perform translations and rotations, respectively.',
			'Use the context menu to change the type of the minutia.',
			'Use the \'Marker size\' slider to globally adjust the size of the minutiae - this will not affect your results and is a display-only setting.',
			'<<mouse:left>>: start drawing',
			'<<mouse:right>>: (while drawing) cancel',
			'<<mouse:left>>: (on a minutia) select and move/rotate the minutia',
			'<<mouse:right>>: (on a minutia) pop up the context menu',
			'<<key:shift>><<mouse:left>>: (hold shift) switch to the edit tool to manipulate the minutia',
			'<<key:M>>: (shortcut) switch to the minutia tool',
			'Note: in PiAnoS, bifurcations point inwards the ridge valley. Ridge endings point up and along the ridge.',
		]);
	}

	// ------------------------------
	// Private variables
	// ------------------------------
	
	var self = this;
	var view = null;
	var line = null;
	var popup = null;
	var options = null;
	var autoPair = false;
	var autoPairInfo = null;
	var markerSize = 4;

	// ------------------------------
	// Private functions
	// ------------------------------
	
	var hidePopup = function()
	{
		popup.menu.remove();
		popup.overlay.remove();
		pianos.leaveModal();
	}
	
	var showPopup = function(docPos)
	{
		/* layout ( content => css class without prefix, prefix is 'pianos4_minutia_popup' )
		
	+-- Choose a minutia type ---------- ...						=> pianos4_minutia_popup
	|	+- class A ------------------+   							=> frame (and label)
	|	|  +---------+  +---------+  | 								=> icon
	|	|  |         |  |         |  |   ...
	|	|  |         |  |         |  |
	|	|  |  bif.   |  |   re.   |  |								=> label
	|	|  +---------+  +---------+  |
	|	+----------------------------+
	|	+---------------+
	|	|  +---------+  |
	|	|  |         |  |
	|	|  |         |  |  ...
	|	|  |  unk.   |  |
	|	|  +---------+  |
	|	+- Class B -----+
	|
	|                                     OK           Cancel		=> don't know if this is really needed	
	+------------------------------------------------------------


	*/
		popup = {
			overlay: $('<div class="ui-widget-overlay" style="width:'+$(document).width()+'px;height:'+$(document).height()+'px;z-index:1000;" />'),
			menu: $('<div class="ui-widget ui-widget-content pianos4_canvas_popup" style="z-index:1001;"></div>'),
			choice: null,
		};
		//width:100px;height:100px;
		var items;
		var labels;
		if (params.v3)
		{
			items = ['bifurcation','ridge_ending','unknown'];
			labels = ['Bifurcation','Ridge ending','Unknown'];
		}
		else
		{
			items = ['bifurcation','ridge_ending','unknown','unknown2'];
			labels = ['Bifurcation','Ridge ending','Type unknown','Position unknown'];
			if (params.phase == 'comparison')
			{
				items.push('difference');
				labels.push('Difference');
			}
		}

		if (params.markCanHaveCenter || view.isPrint())
		{
			items.push('center');
			labels.push('Center');
		}
		
		var ul = $('<ul />');
		for (var i in items)
		{
			var li = $('<li title="'+items[i]+'" class="ui-state-default" />')
				.text(labels[i])
				.bind('mouseenter', function() { $(this).removeClass('ui-state-default').addClass('ui-state-active');  popup.choice = this.title; })
				.bind('mouseleave', function() { $(this).removeClass('ui-state-active').addClass('ui-state-default');  popup.choice = null; })
			;
			ul.append(li);
		}
		popup.menu.append(ul);
		
		
		popup.menu.css('visibility','none');
		$('body').append(popup.overlay).append(popup.menu);
		
		var docSize = [$(window).width(), $(window).height()];
		var menuPos = [
			Math.max(0, Math.min(docSize[0]-popup.menu.outerWidth(), docPos[0])),
			Math.max(0, Math.min(docSize[1]-popup.menu.outerHeight(), docPos[1]))
		];
		popup.menu.css('left', menuPos[0]+'px')
					.css('top', menuPos[1]+'px')
					.removeCSS('visibility');
					
		pianos.enterModal();
	}
}


/* pianos4_tool_pairing.js */
/*
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 Pianos4ToolPairing(params)
{
	// ------------------------------
	// Constructor
	// ------------------------------
	
	params = params || {};
	
	// ------------------------------
	// Public methods
	// ------------------------------
	
	this.getName = function()
	{
		return 'pairing';
	}
	
	this.getLabel = function()
	{
		return 'Pairing';
	}
	
	this.getIcon = function()
	{
		return 'icons/tool_pairing.png';
	}
	
	this.bind = function(aView)
	{
		
		view = aView;
	}
	
	this.unbind = function()
	{
		
		view = null;
	}
	
	this.activate = function()
	{
		
		
		popups = [];
		
		var views = [pianos.getView('mark'), pianos.getView('print')];
		w = $('<div class="pianos4_pairing_hint ui-widget ui-widget-content" />');
		
		for (var i in views)
		{
			var canvas = views[i].getDOM();
			var p = canvas.offset();
			p.left += parseInt(canvas.css('border-left-width'));
			var node = w.clone();
			node.css('left',p.left).css('top',p.top).outerWidth(canvas.width())
				//.append('<span class="ui-icon ui-icon-arrowthick-1-s ui-inline-icon" />')
				.append('<span class="_text">Pick a minutia to start pairing</span>')
				//.append('<span class="ui-icon ui-icon-arrowthick-1-s ui-inline-icon" />')
			;
			$('body').append(node);
			popups.push(node);
			
			var minutiae = views[i].getData().minutiae;
			for (var j in minutiae)
			{
				var m = minutiae[j];
				if ('pairing' in m)
				{
					var g = views[i].getSVG().select('#minutia_'+m.id);
					
					g.attr('stroke-opacity',0.3);
				}
			}
		}
		pianos.toggleLayer('pairing', true);
	}
	
	this.deactivate = function()
	{
		
		for (var i in popups)
			popups[i].remove();
		popups = null;

		var views = [pianos.getView('mark'), pianos.getView('print')];
		for (var i in views)
		{
			var minutiae = views[i].getData().minutiae;
			for (var j in minutiae)
			{
				var m = minutiae[j];
				if ('pairing' in m)
				{
					var g = views[i].getSVG().select('#minutia_'+m.id);
					
					g.attr('stroke-opacity',1.0);
				}
			}
		}
		pianos.toggleLayer('pairing', false);
	}
	
	this.onMouseDown = function(pos, target)
	{
		var me = view.isMark() ? 0 : 1;
		var other = 1-me;
		var paired = null;
		
		if (step == 0)
		{
			
			
			var picked = view.pickByNode(target);
			if (picked && picked.type == 'minutia')
			{
				//
				//
				
				if (eraser)
				{
					
					if ('pairing' in picked.annotation)
						step = 5;
					else
						return true;
				}
				else
				{
					
					options.find('input').attr('disabled', 'disabled');
					paired = picked;
					step = 1;
				}
				
				startView = view;
				startMinutia = picked;
				
				popups[me].addClass('ui-state-highlight').find('._text').text('Release the button');
				popups[other].find('._text').text('-');
			}
		}
		else if (step == 2)
		{
			
			
			if (view.getType() != startView.getType())
			{
				var picked = view.pickByNode(target);
				if (picked && picked.type == 'minutia')
				{
					endMinutia = picked;
					
					//
					//
					
					popups[me].addClass('ui-state-highlight').find('._text').text('Release the button');
					popups[other].find('._text').text('-');
					
					paired = picked;
					step = 3;
				}
			}
		}

		if (paired)
		{
			
		}
	}
	
	this.onMouseMove = function(pos, target)
	{
	}
	
	this.onMouseUp = function(pos, target)
	{
		var me = view.isMark() ? 0 : 1;
		var other = 1-me;
		
		if (step == 1)
		{
			
			popups[me].removeClass('ui-state-highlight').find('._text').text('-');
			popups[other].addClass('ui-state-highlight').find('._text').text('Pick the corresponding minutia');

			step = 2;
		}
		else if (step == 3)
		{
			
			
			var markMin, printMin;
			if (startView.isMark())
			{
				markMin = startMinutia;
				printMin = endMinutia;
			}
			else
			{
				markMin = endMinutia;
				printMin = startMinutia;
			}
			var action = new Pianos4Action_PairMinutiae(markMin.annotation, printMin.annotation, Pianos4Annotation._s_pairIds++);
			
			startView = null;
			startMinutia = null;
			endMinutia = null;
		
			for (var i in popups)
				popups[i].removeClass('ui-state-highlight').find('._text').text('Pick a minutia to continue pairing');
			
			
			options.find('input').removeAttr('disabled');
			
			step = 0;
			
			pianos.exec(action);
		}
		else if (step == 5)
		{
			
			
			var markMin, printMin;
			if (startView.isMark())
			{
				markMin = startMinutia.annotation;
				printMin = null;
			}
			else
			{
				markMin = null;
				printMin = startMinutia.annotation;
			}
			var action = new Pianos4Action_PairMinutiae(markMin, printMin, null);
			
			startView = null;
			startMinutia = null;
			endMinutia = null;
			
			for (var i in popups)
				popups[i].removeClass('ui-state-highlight').find('._text').text('Pick a minutia to remove its pairing');

			step = 0;
			pianos.exec(action);
		}

		return true;
	}
	
	this.getOptionsPanel = function()
	{
		if (!options)
		{
			var panel = $('<div class="pianos4_pairing_options" />');
		
			var randomID = 'pianos4_rid_'+(new Date().getTime());
			var eraserBox = $('<input type="checkbox" '+(eraser?'checked="checked"':'')+' id="'+randomID+'" />');
			eraserBox.bind('click', function() { 
				eraser = this.checked;
				
				var msg = 'Pick a minutia to ';
				if (eraser)
					msg += 'remove its pairing';
				else
					msg += 'start pairing';
					
				for (var i in popups)
					popups[i].find('._text').text(msg);
			});
			var label = $('<label for="'+randomID+'" />').text('Eraser mode');
			
			panel.append($('<div />').append(eraserBox).append(label));
			
			var restart = $('<a />').text('Restart numbering').attr('href','#restart_numbering').bind('click', function(e) {
				e.preventDefault();
				self.restartNumbering();
			});
			
			var remove = $('<a />').text('Remove all pairing').attr('href','#remove_all_pairing').bind('click', function(e) {
				e.preventDefault();
				self.removeAllPairing();
			});

			panel.append($('<div />').css('margin-top','0.5em').append(restart)).append($('<div />').css('margin-top','0.5em').append(remove));

			options = panel;
		}
		
		return options;
	}

	this.restartNumbering = function()
	{
		var batch = new Pianos4Action_Batch('Restart numbering');
		var pairs = {};
		Pianos4Annotation._s_pairIds = 1;

		var mark = pianos.getView('mark');
		var mannotations = mark.getAllAnnotations();
		for (var id in mannotations)
		{
			var a = mannotations[id][1];
			if ('pairing' in a)
			{
				
				pairs[a.pairing] = a;
			}
		}

		var print = pianos.getView('print');
		var pannotations = print.getAllAnnotations();
		for (var id in pannotations)
		{
			var a = pannotations[id][1];
			if ('pairing' in a)
			{
				
				
				var action = new Pianos4Action_PairMinutiae(pairs[a.pairing], a, Pianos4Annotation._s_pairIds++);
				batch.append(action);
			}
		}
		
		pianos.exec(batch);
	}
	
	this.removeAllPairing = function()
	{
		var batch = new Pianos4Action_Batch('Remove all pairing');
		var pairs = {};
		Pianos4Annotation._s_pairIds = 1;

		var mark = pianos.getView('mark');
		var mannotations = mark.getAllAnnotations();
		for (var id in mannotations)
		{
			var a = mannotations[id][1];
			if ('pairing' in a)
			{
				
				var action = new Pianos4Action_PairMinutiae(a, null, null);
				batch.append(action);
			}
		}
		
		pianos.exec(batch);
	}
	
	this.getHelp = function()
	{
		return Pianos4Help.compile([
			'Use the pairing tool to associate (i.e. "match") minutiae on the mark and on the print.',
			'You normally use the pairing tool after all minutiae have been drawn on both images. When it is activated, you must select pairs of minutiae in sequence: one on the mark, then the corresponding one on the print. You can also start by a minutia print and then one on the mark. Once both minutiae have been selected, they are paired and a number appear on top of them. You can then continue pairing until you are satisfied.',
			'When the \'Eraser mode\' is active, a click will remove the pairing of the minutia pointed to - note that the number disappears from both images. You can also remove all pairing using the appropriate option.',
			'Since you cannot choose the pairing number, you may end up with non consecutive numbers and/or a sequence that does not start at 1. Using the \'Restart numbering\' option will re-assign sequential pairing numbers. You generally want to use this option as a final step to make the result look better.',
			'<<mouse:left>>: select a minutia',
			'<<mouse:left>>: (in eraser mode) remove pairing',
			'<<key:P>>: (shortcut) switch to the pairing tool',
			'Note: the pairing numbers are only shown when the \'Pairing\' layer is visible. It is made visible automatically when the pairing tool is activated, but it can also be shown at any moment by using the Layers window.',
		]);
	}
	
	// ------------------------------
	// Private variables
	// ------------------------------
	
	var self = this;
	var view = null;
	var startView = null;
	var startMinutia = null;
	var endMinutia = null;
	var popups = null;
	var step = 0;
	var eraser = false;
	var options = null;
	 
	// ------------------------------
	// Private functions
	// ------------------------------
}


/* pianos4_tool_pan.js */
/*
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 Pianos4ToolPan(params)
{
	// ------------------------------
	// Constructor
	// ------------------------------
	
	params = params || {};
	if (!params.phase) params.phase = 'comparison';
	
	// ------------------------------
	// Public methods
	// ------------------------------
	
	this.getName = function()
	{
		return 'pan';
	}
	
	this.getLabel = function()
	{
		return 'Pan';
	}
	
	this.getIcon = function()
	{
		return 'icons/move.png';
	}
	
	this.bind = function(aView)
	{
		
		view = aView;
	}
	
	this.unbind = function()
	{
		
		view = null;
	}
	
	this.activate = function()
	{
		
		var views = pianos.getViews();
		for (var i in views)
		{
			var svg = views[i].getSVG();
			var g = svg.select('.pianos4_helpers .pianos4_pan_helpers');
			if (g.empty())
			{
				svg.select('.pianos4_helpers').append('svg:g').classed('pianos4_pan_helpers', true);
			}
			else
			{
				g.attr('display', 'inline');
			}
			svg.classed('pianos4_pan_tool_on', true);
		}
	}
	
	this.deactivate = function()
	{
		
		var views = pianos.getViews();
		for (var i in views)
		{
			var svg = views[i].getSVG();
			var g = svg.select('.pianos4_helpers .pianos4_pan_helpers');
			
			g.attr('display', 'none');
			svg.classed('pianos4_pan_tool_on', false);
		}
	}
	
	this.onMouseDown = function(pos, target, event)
	{
		if (event.altKey)
		{
			placing = true;
			
			var g = view.getSVG().select('.pianos4_helpers .pianos4_pan_helpers');
			
			
			g.selectAll('.pianos4_pan_helper').remove();
			
			var segs = [
				[[-20.0, 0.0], [20.0, 0.0]],
				[[0.0, -20.0], [0.0, 20.0]],
			];
			
			var sub = g.append('svg:g')
				.classed('pianos4_pan_helper', true)
				.attr('transform', 'translate('+pos[0]+','+pos[1]+')');
			
			for (var i in segs)
			{
				var s = segs[i];
				sub.append('svg:line')
					.attr('x1', s[0][0])
					.attr('x2', s[1][0])
					.attr('y1', s[0][1])
					.attr('y2', s[1][1])
					.attr('opacity', 0.3)
					.attr('stroke','#880088')
					.attr('stroke-width', 4.0);	
				sub.append('svg:line')
					.attr('x1', s[0][0])
					.attr('x2', s[1][0])
					.attr('y1', s[0][1])
					.attr('y2', s[1][1])
					.attr('opacity', 0.3)
					.attr('stroke','#dd00dd')
					.attr('stroke-width', 2.0);
			}
			
		}
		else
		{
			var views = pianos.getViews();
			origins = {};
			for (var i in views)
			{
				var v = views[i];
				if (!anchored && v.getType() != view.getType())
					continue;
				origins[v.getType()] = v.getOrigin();
				start = pos;
			}
		}
	}
	
	this.onMouseMove = function(pos, target)
	{
		if (!placing)
		{
			var offset = [start[0]-pos[0], start[1]-pos[1]];
			var views = pianos.getViews();
			for (var i in views)
			{
				var v = views[i];
				if (!anchored && v.getType() != view.getType())
					continue;
				var o = [origins[v.getType()][0]+offset[0], origins[v.getType()][1]+offset[1]];
				var vb = v.getSVG().attr('viewBox').split(' ');
				vb[0] = o[0];
				vb[1] = o[1];
				v.getSVG().attr('viewBox', vb.join(' '));
			}
		}
		else
		{
			var g = view.getSVG().select('.pianos4_helpers .pianos4_pan_helper');
					
			g.attr('transform', 'translate('+pos[0]+','+pos[1]+')');
		}
	}
	
	this.onMouseUp = function(pos, target)
	{
		if (!placing)
		{
			var offset = [start[0]-pos[0], start[1]-pos[1]];
			var views = pianos.getViews();
			for (var i in views)
			{
				var v = views[i];
				if (!anchored && v.getType() != view.getType())
					continue;
				var o = [origins[v.getType()][0]+offset[0], origins[v.getType()][1]+offset[1]];
				v.setOrigin(o);
			}
			
			start = null;
			origins = null;
		}
		else
		{
			if (!(view.getType() in anchors))
			{
				// Anchor placed in a window that had none
				anchors.length++;
			}
			else
			{
				// Anchor moved
				if (anchored)
				{
					options.find('input[type=checkbox]').removeAttr('checked');
					setAnchored(false);
				}
			}
			anchors[view.getType()] = $.extend([], true, pos);
			if (anchors.length >= 2)
			{
				options.find('input[type=checkbox]').removeAttr('disabled');
				//options.find('div._hint').hide();
				options.find('label').removeCSS('color');
			}
			placing = false;
		}
		return true;
	}

	this.getOptionsPanel = function()
	{
		if (params.phase == 'analysis')
			return null;
			
		if (!options)
		{
			var panel = $('<div class="pianos4_pan_options" />');
			
			var randomID = 'pianos4_rid_'+(new Date().getTime());
			var disabledStr = '';
			var label = $('<label for="'+randomID+'" />').text('Bind views');
			if (anchors.length < 2)
			{
				disabledStr = ' disabled="disabled"';
				label.css('color','#aaaaaa');
			}
			var anchoredBox = $('<input type="checkbox" '+(anchored?'checked="checked"':'')+' id="'+randomID+'" '+disabledStr+'/>');
			anchoredBox.bind('click', function() { 
				this.blur(); // so that future space+click+drag does not check/uncheck the box
				setAnchored(this.checked);
			});
			panel.append($('<div />').append(anchoredBox).append(label));
			
			panel.append($('<div class="_hint" />').css('margin-top','0.5em').text('Place anchors using ').append($('<strong />').text('Alt+Click')));
			
			options = panel;
		}
		
		return options;
	}
	
	this.getHelp = function()
	{
		return Pianos4Help.compile([
			'Use the pan tool to move the image inside the viewport.',
			'In comparison mode, the two views are not linked and pan independently. You can bind them so that moving one view also moves the other one by the same offset. To make the \'Bind views\' checkbox available, you need to place anchors on both views. Those anchors define points that are deemed to represent the "same" spot. Use the Alt key to place anchors (see below).',
			'<<mouse:left>>: drag to move the view',
			'<<key:space>>: (hold) temporarily switch to the pan tool',
			'<<mouse:middle>>: (shortcut) works like left click, even when the tool is not active',
			'<<key:alt>><<mouse:left>>: place (or move) the anchor',
			'Note: anchors are only visible when the pan tool is active, but the binding is still functional when another tool is active.',
		]);
	}
	
	// ------------------------------
	// Private variables
	// ------------------------------
	
	var self = this;
	var view = null;
	var start = null;
	var origins = null;
	var anchors = {length:0};
	var anchored = false;
	var placing = false;
	var options = null;
	
	
	// ------------------------------
	// Private functions
	// ------------------------------
	
	var setAnchored = function(value)
	{
		anchored = value;
		var views = pianos.getViews();
		for (var i in views)
		{
			var v = views[i];
			if (anchored)
			{
				v.setCenter(anchors[v.getType()]);
				v.setOriginLocked(anchors[v.getType()]);
			}
			else
			{	
				v.setOriginLocked(false);
			}
		}
	}
}


/* pianos4_tool_review.js */
/*
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 Pianos4ToolReview(params)
{
	// ------------------------------
	// Constructor
	// ------------------------------
	
	params = params || {};
	if (!params.lineColor) params.lineColor = '#dd3300';
	if (!params.fillColor) params.fillColor = '#101000';
	
	// ------------------------------
	// Public methods
	// ------------------------------
	
	this.getName = function()
	{
		return 'review';
	}
	
	this.getLabel = function()
	{
		return 'Review';
	}
	
	this.getIcon = function()
	{
		return 'icons/select_analysis.png';
	}
	
	this.bind = function(aView)
	{
		
		view = aView;
	}
	
	this.unbind = function()
	{
		
		view = null;
	}
	
	this.activate = function()
	{
	}
	
	this.deactivate = function()
	{
	}
	
	this.onMouseDown = function(pos, target, event)
	{
		// if (popup)
			// return false;
			
		
		dragSel = {
			from: pos,
			to: pos,
			quad: view.getSVG().append('svg:rect')
					.attr('x', pos[0])
					.attr('y', pos[1])
					.attr('width', 0)
					.attr('height', 0)
					.style('stroke', params.lineColor)
					.style('stroke-width', 1.0/view.getZoomFactor())
					.style('fill', params.lineColor)
					.style('fill-opacity', 0.3)
		};
	}
	
	this.onMouseMove = function(pos, target)
	{
		// if (popup)
			// return false;
			
		if (dragSel)
		{
			dragSel.to = pos;
			var p = $.extend(true,[],dragSel.from);
			var size = [pos[0]-p[0], pos[1]-p[1]];
			for (var i=0;i<2;++i) {
				if (size[i] < 0.0) {
					p[i] += size[i];
					size[i] = Math.abs(size[i]);
				}
			} 
			dragSel.quad.attr('x', p[0]).attr('y', p[1]).attr('width', size[0]).attr('height', size[1]);
			dragSel.tl = p;
			dragSel.br = [p[0]+size[0],p[1]+size[1]];
		}
	}
	
	this.onMouseUp = function(pos, target, event)
	{
		// if (popup)
			// return false;
			
		if (dragSel)
		{
			dragSel.quad.remove();
			
			if ('tl' in dragSel)
			{
				var sel = [];
				var minutiae = view.getData('minutiae').minutiae;
				for (id in minutiae)
				{
					var min = minutiae[id];
					if (min.pos[0] >= dragSel.tl[0] && min.pos[0] <= dragSel.br[0]
					&& min.pos[1] >= dragSel.tl[1] && min.pos[1] <= dragSel.br[1])
						sel.push(min);
				}
				
				if (sel.length)
				{
					showPopup(sel);
				}
			}
			
			dragSel = null;
		}
		return true;
	}
	
	this.cancel = function()
	{
		if (popup)
			hidePopup();
			
		if (dragSel)
		{
			dragSel.quad.remove();
			dragSel = null;
		}

	}

	this.getOptionsPanel = function()
	{
		if (!options)
		{
			var panel = $('<div />').addClass('pianos4_review_options');
			
			panel.append($('<a />').attr('href','#').text('Export ...').click(function(e) {
				e.preventDefault();
				
				var dlg = $('<div />').text('Please choose the exportation format and options:');
				var id_base = 'pianos4_review_opt_'+(new Date().getTime())
				
				var form = $('<form />').attr('action','?action=export').attr('enctype','multipart/form-data').attr('method','post').appendTo(dlg);
				
				var mkradios = function(title, key, items)
				{
					var div = $('<div />').css('margin','0.5em 0');
					
					var id = id_base + '_' + key;
					var title = $('<span />').text(title).css('display','inline-block').css('width','5em');
					div.append(title);
					
					for (var i in items)
					{
						var item = items[i];
						var label = $('<label />').attr('for',id+'_'+i).text(item.label).css('display','inline-block').css('width','7em');
						var opt = $('<input />').attr('id',id+'_'+i).attr('type','radio').attr('name',key).attr('value',item.value);
						
						if (item.disabled)
						{
							label.css('color','#666666');
							opt.prop('disabled',true);
						}
							
						if (i == 0)
							opt.prop('checked',true);
							
						div.append(opt).append(label);
					}
					
					form.append(div);
				};
				
				var views = pianos.getViews();
				var count = 0;
				var vsel = {};
				for (var i in views)
				{
					var view = views[i];
					var sel = view.getSelection();
					for (var j in sel)
						count++;
					vsel[i] = sel;
				}

				mkradios('Subset', 'subset', [
					{ label: 'All', value: 'all' },
					{ label: 'Users ticked', value: 'users' },
					{ label: 'Selection', value: 'selection', disabled:count==0 },
					{ label: 'Visible', value: 'screen', disabled:true },
				]);
				mkradios('Format', 'format', [
					{ label: 'JSON', value: 'json' },
					{ label: 'CSV', value: 'csv' },
				]);
				mkradios('Delivery', 'delivery', [
					{ label: 'Download', value: 'download' },
					{ label: 'View online', value: 'inline' },
				]);
				
				var downloadData = $('<input />').attr('type','hidden').attr('name','data').appendTo(form);
				var exidData = $('<input />').attr('type','hidden').attr('name','exid').appendTo(form);
				
				var opts = {
					title: 'Export results',
					width: 600,
					modal: true,
					buttons: {
						'OK': function() {
							var settings = form.serializeObject();
							var indata = null;
							var exid = getURLParameter('id');
							if (settings.subset == 'users')
							{
								var vmap = pianos.getUsersResultsVisibility();
								indata = pianos.getData(null, function(annot,view) {
									
									return vmap[annot.userid];
								});
							}
							else if (settings.subset == 'selection')
							{
								indata = pianos.getData(null, function(annot, view) {
									var vtype = view.getType();
									if (!(vtype in vsel))
										return false;
									var sel = vsel[vtype];
									return annot.id in sel;
								});
							}
							else if (settings.subset == 'screen')
							{
								
							}
							else
							{
								indata = pianos.getData();
							}
							
							var data = {};
							var serverData = pianos.getServerData();
							var serverDataIdx = null;
							if (serverData)
							{
								// serverData is the complete dataset retrieved from the server (or per initialization)
								// It contains the annotations as they were lastly fetched (which may differ from
								// the annotations currently shown/stored in the various controls of the UI) and also
								// an additionnal piece of info we want to export: the observations.
								// It's a mere array of results, and needs to be indexed by user for fast retrieval.
								serverDataIdx = {};
								for (var i in serverData)
								{
									var result = serverData[i];
									if ('observations' in result)
									{
										
										serverDataIdx[result.user.id] = i;
									}
								}
							}
							
							for (var vtype in indata)
							{
								var invdata = indata[vtype];
								var vdata = {
									'image': null,
									'users': {}
								};
								for (atype in invdata)
								{
									if (atype == 'image')
									{
										vdata[atype] = invdata[atype];
									}
									else
									{
										var inadata = invdata[atype];
										for (var aid in inadata)
										{
											var annot = inadata[aid];
											var udata = null;
											if (!(annot.userid in vdata['users']))
											{
												udata = {};
												if (serverDataIdx && annot.userid in serverDataIdx)
												{
													
													udata['observations'] = $.extend(true, {}, serverData[serverDataIdx[annot.userid]].observations);
												}
												else
												{
													udata['observations'] = {};
												}
												vdata['users'][annot.userid] = udata;
											}
											else
												udata = vdata['users'][annot.userid];
											
											var tdata = null;
											if (!(atype in udata))
											{
												tdata = {};
												udata[atype] = tdata;
											}
											else
												tdata = udata[atype];
												
											var copy = $.extend(true, {}, annot);
											delete copy.userid;
											tdata[aid] = copy;
										}
									}
								}
								
								data[vtype] = vdata;
							}
							
							if (settings.delivery == 'download')
							{
								downloadData.val(JSON.stringify(data));
								exidData.val(exid);
								form.submit();
							}
							else
							{
								pianos.ajax({
									pendingMessage: 'Exporting',
									type: 'post',
									processData: false,
									contentType: 'application/json',
									dataType: 'text',
									url: '?action=export',
									data: JSON.stringify($.extend(settings, {
										data: data,
										exid: exid,
									})),
									success: function(data) {
										showExportDialog(data);
									},
									error: function(xhr, status, error) {
										alert('HTTP error (code '+xhr.status+'): '+(error?error:'connection failed'));
									},
								});
							}
							
							$(this).dialog('close');
						},
						'Cancel': function() { $(this).dialog('close'); }
					},
					close: function() { $(this).remove(); },
				};
				
				pianos.dialog(dlg, opts);
				
			}));
			
			options = panel;
		}
		
		return options;
	}
	
	
	// ------------------------------
	// Private variables
	// ------------------------------
	
	var self = this;
	var view = null;
	var dragSel = null;
	var popup = null;
	var options = null;
	
	// ------------------------------
	// Private functions
	// ------------------------------

	var showPopup = function(sel)
	{
		
		
		if (!popup)
		{
			popup = $('<div class="pianos4_tool_review_popup" />');

			popup.dialog({
				title: 'Annotations by user',
				autoOpen: false,
				width: 500,
				buttons: {
					'OK': function() {
						hidePopup();
					},
				},
			});
		}
		
		var div = $('<div />');
		var overview = $('<table class="overview"><thead><tr /></thead><tbody></tbody></table>').appendTo(div);
		var details = $('<table class="details"><thead><tr /></thead><tbody></tbody></table>');
		
		
		var rowClasses = ['even','odd'];
		var minutiaTypes = {
			'bifurcation': 'BIF',
			'ridge_ending': 'END',
			'unknown2': 'TUNK',
			'unknown': 'PUNK',
			'difference': 'DIFF',
			'center': 'CENT',
		};
		
		var users = pianos.getUsers();
		var minutiae = {'-1':{}};
		for (var j in minutiaTypes) // We store the sum of all annotations from all users at the -1 index
			minutiae[-1][j] = []; 
		users[-1] = {id:-1,name:'All users'};
		
		details.find('thead tr')
			.append($('<th />').text('User'))
			.append($('<th />').text('ID'))
			.append($('<th />').text('Type'))
			.append($('<th />').text('X'))
			.append($('<th />').text('Y'))
			.append($('<th />').text('Angle'))
			.append($('<th />').text('Pair'));
		
		for (var i in sel)
		{
			var annotation = sel[i];
			if (view.getAnnotationType(annotation.id) == 'minutia')
			{
				
				var user = users[annotation.userid];
				
				if (!(user.id in minutiae))
				{
					minutiae[user.id] = {};
					for (var j in minutiaTypes)
						minutiae[user.id][j] = [];
				}
				minutiae[user.id][annotation.type].push(annotation.id);
				minutiae[-1][annotation.type].push(annotation.id);
				
				var tr = $('<tr />').addClass(rowClasses[i%2]);
				tr.append($('<td />').text(user.name));
				tr.append($('<td />').text(user.id));
				tr.append($('<td />').text(minutiaTypes[annotation.type]));
				tr.append($('<td />').text(annotation.pos[0].toFixed(2)));
				tr.append($('<td />').text(annotation.pos[1].toFixed(2)));
				tr.append($('<td />').text(annotation.angle.toFixed(2)));
				tr.append($('<td />').text('pairing' in annotation ? annotation.pairing : ''));
				details.append(tr);
			}
		}
		
		var overview_labels = {'BIF':'Bifurcation','END':'Ridge ending','TUNK':'Type unknown','PUNK':'Position unknown', 'DIFF':'Difference', 'CENT':'Center'};
		var overview_head_tr = overview.find('thead tr');
		overview_head_tr.append($('<th />').text('Author'));
		for (var j in minutiaTypes)
		{
			overview_head_tr.append($('<th class="rotated" />').text(overview_labels[minutiaTypes[j]]));
		}
		
		var currentView = view;
		var overview_body = overview.find('tbody');
		i=0;
		for (var userid in minutiae)
		{
			var user = users[userid];
			var data = minutiae[userid];
			
			var tr = $('<tr />').addClass(rowClasses[i++%2]);
			tr.append($('<td class="username" />').text(user.name));
			for (var j in minutiaTypes) (function(mids)
			{
				var td = $('<td />');
				if (mids.length > 0)
				{
					$('<span />').addClass('nonzero').text(mids.length).data('pianos4_review',mids)
						.bind('mouseenter', function(e) {
							var d = $(this).data('pianos4_review');
							toggleMinutiae(currentView, d, true);
						}).bind('mouseleave', function(e) {
							var d = $(this).data('pianos4_review');
							toggleMinutiae(currentView, d, false);
						}).appendTo(td);
				}
				tr.append(td);
			})(data[j]);
			overview_body.append(tr);
		}
		overview_body.find('tr:first').addClass('allusers');
		
		var hint = $('<div class="details_hint" />')
			.append($('<span />').text('To paste into Excel: '))
			.append($('<a href="#" />').text('select the data').bind('click', function(e) { 
				e.preventDefault();
				//$(this).parent().siblings('table').get(0).select();
				selectElementContents($(this).parent().siblings('table').get(0));
			}))
			.append($('<span />').text(', copy and click Paste > Paste Special > As Unicode Text'));
		
		div.append($('<div />').append('<span class="toggle_details ui-icon ui-inline-icon ui-icon-triangle-1-e" />').append($('<a href="#toggle" class="toggle_details" />').text('Toggle details').bind('click', function(e) {
			e.preventDefault();
			$(this).prev().toggleClass('ui-icon-triangle-1-s');
			$(this).next().toggle();
		})).append($('<div />').append(details).append(hint).hide()));
		
		var content = popup.find('>div');
		content.remove();
		popup.append(div);
		
		popup.css('max-height', Math.max(200,$(window).height()-200)+'px');
		
		popup.dialog('open');
	}
	
	var hidePopup = function()
	{
		
		popup.dialog('close');
		popup.remove();
		popup = null;
	}
	
	var toggleMinutiae = function(currentView, ids, on)
	{
		var layer = currentView.getSVG().select('.pianos4_minutiae');
		var clonables = {};
		for (var i in ids)
		{
			var mid = ids[i];
			if (on)
			{
				// We want to display an overlay over the reviewed minutia. We can't just re-use the gizmo because
				// attributes of a <use> node cannot override those of the referenced node - namely, the color. So
				// we find the node referenced by the gizmo, clone it, modify the relevant attributes and insert it
				// into the SVG. This also looks because unlike jQuery, d3.js does not have a .clone() method.
				var sel = layer.select('#minutia_'+mid+' ._sel');
				var nodeID = sel.attr('xlink:href');
				if (!(nodeID in clonables))
					clonables[nodeID] = currentView.getSVG().select('defs '+nodeID).node();
				var toClone = clonables[nodeID];
				var cloned = toClone.cloneNode(true);
				d3.select(cloned).attr('id',null).classed('_review', true).attr('stroke',params.lineColor).attr('fill',params.lineColor).attr('fill-opacity','0.3');
				sel.node().parentNode.appendChild(cloned);
			}
			else
			{
				var m = layer.select('#minutia_'+mid+' ._review');
				m.remove();
			}
		}
	}
	
	var showExportDialog = function(data)
	{
		var div = $('<div />');
		
		var pre = $('<pre />').appendTo(div);
		
		pre.append(data);
		
		pianos.dialog(div, {
			title: 'Exported data',
			close: function() { $(this).remove(); },
			width: 500,
			height: Math.max(150,$(window).height()*90.0/100.0),
			buttons: {
				'Close': function() { $(this).dialog('close'); }
			},
		});
	}
}


/* pianos4_tool_ridge.js */
/*
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 Pianos4ToolRidge(params)
{
	// ------------------------------
	// Constructor
	// ------------------------------
	
	params = params || {};
	if (typeof(params.type) == 'undefined') params.type = 'ridge';
	
	if (!params.closeRidgeThreshold) params.closeRidgeThreshold = 2.0;
	if (!params.closeAreaThreshold) params.closeRidgeThreshold = 4.0;
	if (!params.intersectScript) params.intersectScript = null;
	if (params.type == 'quality' && !params.qualityTypeSelector) params.qualityTypeSelector = 'checkbox';
			// other possible values: 'dropdown' or something else (don't show the controller)
	if (params.type == 'quality' && typeof(params.qualityDefaultAnsiNist) == 'undefined') params.qualityDefaultAnsiNist = true;
	
	
	// ------------------------------
	// Public methods
	// ------------------------------
	
	this.getName = function()
	{
		return params.type;
	}
	
	this.getLabel = function()
	{
		switch (params.type)
		{
		default:
		case 'ridge': return 'Ridge';
		case 'area': return 'Area';
		case 'quality': return 'Quality';
		case 'wcs': return 'Other features';
		}
	}
	
	this.getIcon = function()
	{
		switch (params.type)
		{
		default:
		case 'ridge': return 'icons/tool_ridge.png';
		case 'area': return 'icons/draw.png';
		case 'quality': return 'icons/tool_quality.png';
		case 'wcs': return 'icons/tool_other.png';
		}
	}
	
	this.bind = function(aView)
	{
		
		view = aView;
	}
	
	this.unbind = function()
	{
		
		view = null;
	}
	
	this.activate = function()
	{
	}
	
	this.deactivate = function()
	{
	}
	
	this.onMouseDown = function(pos)
	{
		if (points.length == 0)
		{
			// A first click creates the ridge/area
			
			
			
			path = view.getSVG().append('svg:path')
				.style('stroke', color)
				.style('stroke-width', 3.0/view.getZoomFactor())
				.style('fill','none')
				;
			line = view.getSVG().append('svg:line')
				.style('stroke', color)
				.style('stroke-width', 3.0/view.getZoomFactor())
				.style('stroke-dasharray', 7.0/view.getZoomFactor()+' '+4.0/view.getZoomFactor())
			;
			
			if (params.type == 'ridge')
				pianos.toggleLayer('ridges', true);
			else if (params.type == 'wcs')
				pianos.toggleLayer('others', true);
			else
				pianos.toggleLayer('areas', true);
		}
		else
		{
			// A click on the last point (with a little tolerance) terminates the ridge.
			// When the tool is in 'area' mode, a click on the first point terminates
			// the area - a click on the last point also terminates the area by filling the
			// gap to the first point.
			var lastPos = [parseFloat(line.attr('x1')), parseFloat(line.attr('y1'))];
			var closeToLastPoint = getDistance(lastPos, pos) < params.closeRidgeThreshold;
			var closeToFirstPoint = params.type == 'area' && points.length>0 && getDistance(points[0], pos) < 2.0*params.closeAreaThreshold; 
			if (closeToLastPoint || closeToFirstPoint)
			{
				line.remove();
				line = null;
				path.remove();
				path = null;
				
				var nextColor = false;
				
				if (params.type == 'ridge' && points.length >= 2)
				{
					var action = new Pianos4Action_CreateAnnotation(view, 'ridge', new Pianos4Ridge(points, color, width, opacity));
					pianos.exec(action);
					nextColor = true;
				}
				else if ((params.type == 'area' || params.type == 'quality' || params.type == 'wcs') && points.length >= 3)
				{
					var area;
					if (params.type == 'quality')
						area = new Pianos4Area(points, color, 'quality', getConstantIdByColor(color, Pianos4Constants.quality[ansiNistQuality?'ansi_nist':'standard']));
					else if (params.type == 'wcs')
						area = new Pianos4Area(points, color, 'wcs', getConstantIdByColor(color, Pianos4Constants.details));
					else
						area = new Pianos4Area(points, color)
					
					var action = new Pianos4Action_CreateAnnotation(view, 'area', area);
					pianos.exec(action);
					nextColor = true;
				}
				// If the ridge/area is finished but does not contain enough points, just discard it.
				
				
				if (options && nextColor && cycle)
				{
					var curCol = options.find('.pianos4_tool_color[title='+color+']');
					if (curCol.length == 1)
					{
						var nextCol = curCol.next('.pianos4_tool_color');
						if (nextCol.length == 0)
							nextCol = options.find('.pianos4_tool_color:first');
						if (nextCol.length == 1)
						{
							curCol.removeCSS('border-color');
							nextCol.css('border-color','#aaaaaa');
							color = nextCol.attr('data-color');
						}
					}
				}
				
				points = [];
				return true;
			}
		}
		
		line
			.attr('x1', pos[0])
			.attr('y1', pos[1])
			.attr('x2', pos[0])
			.attr('y2', pos[1])
		;

		points.push(pos);
		
		updatePath();
	}
	
	this.onMouseMove = function(pos)
	{
		line
			.attr('x2', pos[0])
			.attr('y2', pos[1])
		;
	}
	
	this.onMouseUp = function(pos)
	{
	}
	
	this.getOptionsPanel = function()
	{
		if (!options)
		{
			var panel = $('<div class="pianos4_ridge_options" />');
		
			var selectColorCallback = function()
			{
				if ($(this).attr('data-color') != color)
				{
					color = $(this).attr('data-color');
					$(this).parents('._content').find('.pianos4_tool_color').removeCSS('border-color');
					$(this).css('border-color','#aaaaaa');
				}
			};
			
			var colorScale = null;
			var showCycleColors = true;
			if (params.type == 'quality')
			{
				var randomID = 'pianos4_rid_'+(new Date().getTime());
				if (params.qualityTypeSelector == 'dropdown')
				{
					var ansiNistSelector = $('<select id="'+randomID+'" />');
					ansiNistSelector
						.append($('<option value="0" />').text('Standard'))
						.append($('<option value="1" />').text('ANSI/NIST'))
						.css('margin-bottom', '3px')
						.change(function() {
							setAnsiNistQualityMode(parseInt($(this).val())>0);
						})
					;
					var initSel = ansiNistQuality ? '1' : '0';
					ansiNistSelector.find('option[value='+initSel+']').attr('selected','selected');
					panel.append($('<div />').append(ansiNistSelector));
				}
				else
				{
					var ansiNistBox = $('<input type="checkbox" '+(ansiNistQuality?'checked="checked"':'')+' id="'+randomID+'" />');
					ansiNistBox.bind('click', function() { setAnsiNistQualityMode(this.checked); });
					var label = $('<label for="'+randomID+'" />').text('ANSI/NIST');
					panel.append($('<div />').append(ansiNistBox).append(label));
				}
				

				var ansiNistDiv = $('<div class="_ansi_nist" />').hide();
				Pianos4ToolRidge.createColorPicker(ansiNistDiv, selectColorCallback, 'quality_ansi_nist');
				var standardDiv = $('<div class="_standard" />').hide();
				Pianos4ToolRidge.createColorPicker(standardDiv, selectColorCallback, 'quality_std');
				
				// Tweak the color picker returned by createColorPicker to show the "primary quality value"
				// widget.
				//var width = $('.pianos4_toolbox').children('.ui-widget-content').width();
				var getpos = function(obj, ev) { var p=obj.offset(); return {left:ev.pageX-p.left,top:ev.pageY-p.top}; };
				$([ansiNistDiv, standardDiv]).each(function() {
					var $this = $(this);
					var bkg = $('<div />').addClass('pianos4_labelled_color_bkg').prependTo($this);
					var dragstart = null;
					var dragstartp = null;
					var onmup = function(e) {
						e.preventDefault();
						$(document).unbind('mouseup', onmup);
						$(document).unbind('mousemove', onmmove);
						dragstart = null;
						dragstartp = null;
						var newPrimaryQuality = -1;
						bkgpos = bkg.position();
						var lowestValue = 1e100;
						var i = 0;
						$this.children('.pianos4_labelled_color').each(function() {
							var offs = Math.abs($(this).position().top-bkgpos.top);
							if (offs < lowestValue) {
								newPrimaryQuality = i;
								lowestValue = offs;
							}
							++i;
						});
						if (newPrimaryQuality<0) 
							newPrimaryQuality=0;
						setPrimaryQuality($this, newPrimaryQuality);
					};
					var onmmove = function(e) {
						if (dragstart)
						{
							e.preventDefault();
							var pos = {left:e.pageX,top:e.pageY};
							var bkgtop = pos.top - dragstart.top + dragstartp.top;
							bkgtop = Math.min(Math.max($this.position().top, bkgtop), $this.position().top+$this.height()-bkg.height());
							bkg.css('top', bkgtop);
						}
					};
					bkg.addClass('ui-corner-all')
						.append($('<span />').addClass('_bkg-label').text('main').bind('mousedown', function(e) { 
										e.preventDefault();
										dragstart = {left:e.pageX,top:e.pageY};
										dragstartp = bkg.position();
										$(document).bind('mousemove', onmmove);
										$(document).bind('mouseup', onmup);
									})
					);
					$(this).children('.pianos4_labelled_color').each(function() { $(this).css('position','relative').css('z-index','1'); });
				});
				
				if (ansiNistQuality)
					ansiNistDiv.show();
				else
					standardDiv.show();
					
				panel.append(ansiNistDiv);
				panel.append(standardDiv);
				
				showCycleColors = false;
			}
			else if (params.type == 'wcs')
			{
				colorScale = 'wcs';
				showCycleColors = false;
			}
			else colorScale = 'standard';
				
			if (colorScale)
			{
				Pianos4ToolRidge.createColorPicker(panel, selectColorCallback, colorScale);
			}
			
			var initialColor;
			if (params.type != 'quality')
				initialColor = panel.find('.pianos4_tool_color:first');
			else
				initialColor = panel.find((ansiNistQuality?'._ansi_nist':'._standard')+' .pianos4_tool_color:first');
			color = initialColor.attr('data-color');
			initialColor.css('border-color','#aaaaaa');

			if (params.type == "ridge")
			{
				panel.append(createToolSlider('Width', {
					min:1,
					max:20,
					value:width,
					onSlide:function(value) { 
						width = value; 
					},
					unit: 'pt',
				}));
				
				panel.append(createToolSlider('Opacity', {
					min:1,
					max:100,
					value:100.0*opacity,
					onSlide:function(value) { 
						opacity = value/100.0; 
					},
					unit: '%',
				}));
			}
			
			if (showCycleColors)
			{
				var randomID = 'pianos4_rid_'+(new Date().getTime());
				var cyclebox = $('<input type="checkbox" '+(cycle?'checked="checked"':'')+' id="'+randomID+'" />');
				cyclebox.bind('click', function() { cycle = this.checked; });
				var label = $('<label for="'+randomID+'" />').text('Cycle colors');
				
				panel.append($('<div />').append(cyclebox).append(label));
			}
			else
			{
				cycle = false;
			}

			if (params.type == 'quality' && params.intersectScript)
			{
				var computeFinalQMap = $('<a />').text('Show final quality map').attr('href','#compute_final_qmap').bind('click', function(e) {
					e.preventDefault();
					computeFinalQualityMap();
				});
				panel.append($('<div />').append(computeFinalQMap));
			}

			options = panel;
		}
		
		return options;
	}
	
	this.cancel = function()
	{
		if (line)
			line.remove();
		line = null;
		if (path)
			path.remove();
		path = null;
		points = [];
	}
	
	this.isQualityAnsiNist = function()
	{
		
		return ansiNistQuality;
	}
	
	this.onColumnLayout = function()
	{
		if (options && params.type == 'quality')
		{
			$([options.find('._ansi_nist'), options.find('._standard')]).each(function() {
				setPrimaryQuality(this, primaryQuality);
			});
		}
	}
	
	this.getHelp = function()
	{
		if (params.type == 'ridge')
		{
			return Pianos4Help.compile([
				'Use the ridge tool to draw ridges. First, choose a color using the palette. Start drawing by clicking a first point, then click again to place as many points as desired along the ridge, and finish by double-clicking the final point.',
				'After a ridge has been drawn, you can add/move/delete control points using the edit tool. Once the ridge is selected, handles appear on the control points (white circles). These can be moved when the mouse cursor changes to a cross. To add a point, right click inside a segment and choose \'Add point here\'. To remove a point, right click the point and choose \'Remove this point\'.',
				'Still using the edit tool, click and drag inside a segment to move the entire ridge.',
				'The color of a ridge can be changed using the \'Properties\' window.',
				'<<mouse:left>>: place ridge point',
				'<<mouse:left>>: (double-click) place final point',
				'<<mouse:right>>: (while drawing) cancel',
				'<<mouse:right>>: (on a ridge) pop up the context menu',
				'<<key:shift>><<mouse:left>>: (hold shift) switch to the edit tool to manipulate the ridge',
				'<<key:R>>: (shortcut) switch to the ridge tool',
				'Use the \'Width\' and \'Opacity\' sliders to adjust the tickness and the transparency of ridges to be drawn next. The defaults should be pretty sensible, however.',
				'When the \'Cycle colors\' option is on, consecutive ridges will be drawn in different colors. Uncheck the box if you want to draw many ridges of the same color.',
			]);
		}
		else if (params.type == 'quality')
		{
			return Pianos4Help.compile([
				'Use the quality tool to quality areas (polygons). First, choose a color using the palette. Start drawing by clicking a first point, then click again to place as many points as desired along the polygon\'s boundaries, and finish by double-clicking the last point (the final segment between the last and first points, closing the polygon, will be inserted automatically).',
				'After an area has been drawn, you can add/move/delete control points using the edit tool. Once the area is selected, handles appear on the control points (white circles). These can be moved when the mouse cursor changes to a cross. To add a point, right click inside a segment and choose \'Add point here\'. To remove a point, right click the point and choose \'Remove this point\'.',
				'Still using the edit tool, click and drag inside a segment to move the entire area.',
				'The color of a area can be changed using the \'Properties\' window.',
				'<<mouse:left>>: place area point',
				'<<mouse:left>>: (double-click) place final point',
				'<<mouse:right>>: (while drawing) cancel',
				'<<mouse:right>>: (inside an area) pop up the context menu',
				'<<key:shift>><<mouse:left>>: (hold shift) switch to the edit tool to manipulate the area',
				'<<key:Q>>: (shortcut) switch to the quality tool',
				'The gray rectangle labeled \'main\' can be dragged up and down. It is used to choose the dominant quality of the image. This is only useful when you use the \'Show final quality map\' feature. [[Read more information about quality here|quality]].',
			]);
		}
		else if (params.type == 'wcs')
		{
			return Pianos4Help.compile([
				'Use the other features tool to draw wrinkles, creases and scars. These are drawn as polygons. First, choose a type using the palette. Start drawing by clicking a first point, then click again to place as many points as desired along the polygon\'s boundaries, and finish by double-clicking the last point (the final segment between the last and first points, closing the polygon, will be inserted automatically).',
				'After an area has been drawn, you can add/move/delete control points using the edit tool. Once the area is selected, handles appear on the control points (white circles). These can be moved when the mouse cursor changes to a cross. To add a point, right click inside a segment and choose \'Add point here\'. To remove a point, right click the point and choose \'Remove this point\'.',
				'Still using the edit tool, click and drag inside a segment to move the entire area.',
				'The color of a area can be changed using the \'Properties\' window.',
				'<<mouse:left>>: place area point',
				'<<mouse:left>>: (double-click) place final point',
				'<<mouse:right>>: (while drawing) cancel',
				'<<mouse:right>>: (inside an area) pop up the context menu',
				'<<key:shift>><<mouse:left>>: (hold shift) switch to the edit tool to manipulate the area',
				'<<key:O>>: (shortcut) switch to the other features tool',
			]);
		}
	}

	// ------------------------------
	// Private variables
	// ------------------------------
	
	var self = this;
	var view = null;
	var path = null;
	var line = null;
	var points = [];
	var color = '#ff0000';
	var width = 5.0;
	var opacity = 0.85;
	var options = null;
	var cycle = true;
	var ansiNistQuality = params.qualityDefaultAnsiNist;
	var primaryQuality = 0;
	
	// ------------------------------
	// Private functions
	// ------------------------------
	
	var updatePath = function()
	{
		if (points.length >= 2)
		{
			var def = 'M ';
			for (var i in points)
			{
				if (i>0)
					def += ' L';
				def += ' '+points[i][0]+' '+points[i][1];
			}
			path.attr('d', def);
		}
	}
	
	var getDistance = function(pos0, pos1)
	{
		var p0 = view.getAbsPos(pos0);
		var p1 = view.getAbsPos(pos1);
		var v = [p1[0]-p0[0], p1[1]-p0[1]];
		var n = Math.sqrt(v[0]*v[0]+v[1]*v[1]);
		return n;
	}
	
	var setAnsiNistQualityMode = function(value, mode)
	{
		if (value === ansiNistQuality)
			return;
		
		// If 'mode' is 'ui', only the user interface and the internal state
		// are updated - it is assumed that conversion-related stuff is handled
		// elsewhere. This is currently only the case in the undo/redo action
		// associated with a previous conversion.
		if (mode != 'ui')
		{
			// Changing the quality mode while quality zones have already been drawn is
			// a non-trivial operation: we need to change them, otherwise their values
			// will not match the current scale (e.g. a zone in standard quality 2 (medium)
			// would become a zone in ANSI/NIST quality 2 (Definitive Ridge Edges), which
			// is not what we want).
			
			// So check if there zones have already been drawn (except if 'mode' is
			// 'force', which is only the case when the following code has been run and the
			// user pressed 'OK' in the dialog)
			var conversionNeeded = false;
			var unused = {};
			var views = pianos.getViews();
			for (var i in views)
			{
				var view = views[i];
				var data = getQualityMaps(view, unused);
				if (data.length)
				{
					conversionNeeded = true;
					break;
				}
			}

			if (mode != 'force' && conversionNeeded)
			{
				var dlg = $('<div />');
				var scales = ['ANSI/NIST','standard'];
				var curScale = ansiNistQuality ? 0 : 1;
				dlg.append($('<p />').text('You have already drawn quality zones using the '+scales[curScale]+' scale, and you are trying to switch to the '+scales[1-curScale]+' scale.'));
				dlg.append($('<strong />').text('This operation will perform a lossy conversion, as values of the two scales do not map 1:1.').appendTo($('<p />')));
				dlg.append($('<p />').text('You can still undo the operation if you are not satisfied with the results'));
				pianos.dialog(dlg, {
					title: 'Quality conversion needed',
					width: 500,
					modal: true,
					buttons: {
						'Proceed with the conversion': function() {
							$(this).dialog('close');
							setAnsiNistQualityMode(value, 'force');
						},
						'Cancel': function() {
							$(this).dialog('close');
							if (params.qualityTypeSelector == 'dropdown')
								options.find('select').val(ansiNistQuality?'1':'0');
							else
								options.find('input[type=checkbox]').prop('checked',ansiNistQuality?true:false);
						},
					},
					close: function() {
						dlg.remove();
					},
				});
				return;
			}
			
			if (conversionNeeded)
			{
				convertToAnsiNistQualityMode(value);
			}
		}
		
		ansiNistQuality = value;
		primaryQuality = 0;

		options.find('._ansi_nist').toggle();
		options.find('._standard').toggle();
		
		var selCol = options.find('.pianos4_tool_color:visible:first');
		color = selCol.attr('data-color');
		selCol.parents('._content').find('.pianos4_tool_color').removeCSS('border-color');
		selCol.css('border-color','#aaaaaa');
		
		pianos.reLayoutLeftColumn();
	}
	
	var convertToAnsiNistQualityMode = function(value)
	{
		
		var convMap = Pianos4Constants.quality.conversions[value?'std_to_an':'an_to_std'];
		var views = pianos.getViews();
		var batch = new Pianos4Action_Batch('Quality conversion');

		for (var i in views)
		{
			var view = views[i];
			var data = view.getData('areas', function(item) {
				return 'type' in item && item.type == 'quality';
			});
			var ret = [];
			
			for (var i in data.areas)
			{
				var area = data.areas[i];
				
				
				
				var newVal = convMap[area.value];
				var mods = {
					color: getConstantColorById(newVal, Pianos4Constants.quality[value?'ansi_nist':'standard']),
					value: newVal,
				};
				
				var action = new Pianos4Action_ModifyAnnotation(view, area, mods);
				batch.append(action);
			}
		}

		// The undo/redo action is not sufficient in itself: it will properly update the
		// annotations, but the UI panel of this tool will be left in the incorrect state
		// after the first undo and subsequent redos/undos. We add a custom action to
		// handle this case, using a closure so that we do not mess with further calls
		// to this function.
		;(function() {
			var firstRun = true;
			var valueToSet = value;
			var controlSetter = function(v) {
				if (params.qualityTypeSelector == 'dropdown')
					options.find('select').val(v?'1':'0');
				else
					options.find('input[type=checkbox]').prop('checked',v?true:false);
			};
			var undo = function() {
				 // Can only undo if it has been redo()ed once (during pianos.exec())
				setAnsiNistQualityMode(!valueToSet, 'ui');
				controlSetter(!valueToSet);
			};
			var redo = function() {
				if (firstRun)
				{
					// When pianos.exec() is called, it will call redo() but everything
					// has been taken care of, so do nothing.
					firstRun = false;
					return;
				}
				setAnsiNistQualityMode(valueToSet, 'ui');
				controlSetter(valueToSet);
			};
			var action = new Pianos4Action_Custom(redo, undo);
			batch.append(action);
		})();
		
		pianos.exec(batch);
	}
	
	var getConstantIdByColor = function(color, array)
	{
		for (var i in array)
		{
			var item = array[i];
			if (item.color == color)
				return item.id;
		}
		throw 'Could not find the color '+color+' in the list of constants';
	}
	
	var getConstantColorById = function(id, array)
	{
		for (var i in array)
		{
			var item = array[i];
			if (item.id == id)
				return item.color;
		}
		throw 'Could not find the id '+id+' in the list of constants';
	}

	var getQualityMaps = function(view, layerMapping)
	{
		// Returns a JSON object that forms valid input to the C++ "intersect_area" action. This
		// object must be an array of the form [[id,value,points,rings],...]
		// Elements represent unique polygons in PiAnoS. 
		// 'id' is currently ununsed by the C++ side (fixme?)
		// 'value' is the layer. This is the order defines which layers "cut holes" into which 
		//         layers. On output, the first layer is the first input layer minus all subsequent layers. The
		//         last layers remains thus unaffected by other layers. Each layer is "unioned" by the algorithm.
		// 'points' is the list of points forming the polygon (no repetition of the first vertex in last position)
		// 'rings' is the list of rings (polygons that are fully inside another polygon, ordered in opposite
		//         direction)
		
		// In PiAnoS, the user cannot create rings directly*: only the C++ script can. But it will happily 
		// store them and use them in subsequent uses of the "Set final quality map" feature.
		// (*) this explains why users cannot edit the rings as well
		
		var data = view.getData('areas', function(item) {
			return 'type' in item && item.type == 'quality';
		});
		var ret = [];
		
		var lm = layerMapping[view.getType()] || {};
		for (var i in data.areas)
		{
			var area = data.areas[i];
			

			// 'area.value' is a constant in Pianos4Constants.quality.xxx
			// 'primaryQuality' is the list item selected in the UI (equals a constant minus 1)
			// It is used to compute the order in which polygons will be sent: the "primary quality" set in
			// the UI (by default, the best quality) must be first in the list - so other quality zones cut
			// holes in it - because it is the one that will be drawn first and will mostly cover all areas.
			// The order chosen for the subsequent layers is quite experimental: (p is the primary layer)
			// [p, p-1, p+1, p-2, p+2, ...]
			
			// Thus given a=(primaryQuality-area.value+1), the resulting layer index=i is
			// a = 0  -> i = 0
			// a = -1 -> i = 1
			// a = +1 -> i = 2
			// a = -2 -> i = 3
			// ...
			// a = n  -> i = 2.abs(a)-1     if a > 0
			//             = 2.abs(a)       if a <= 0
			var a = primaryQuality-area.value+1;
			var ispos = a > 0 ? 1 : 0;
			var layer = 2*Math.abs(a)-ispos;
			lm[layer] = area.value;
			var adata = [area.id, layer, area.points, []];
			if ('holes' in area)
				adata[3] = area.holes;
			ret.push(adata);
		}
		layerMapping[view.getType()] = lm;
		
/*		ret.sort(function(left,right) {
			// Sort from lowest to highest quality
			// (the higher the value, the lower the quality, see Pianos4Constants.js)
			return left[1] < right[1];
		});
		Actually, sorting is not needed here since the C++ part already sorts according the
		second element 'value' of the polygon data (=)
		*/
		
		return ret;
	}
	
	var computeFinalQualityMap = function()
	{
		
		
		var post = {};
		var views = pianos.getViews();
		var numDatum = 0;
		var layerMapping = {};
		for (var i in views)
		{
			var view = views[i];
			var data = getQualityMaps(view, layerMapping);
			if (data.length)
			{
				post[view.getType()] = data;
				numDatum += 1;
			}
		}
		
		if (numDatum == 0)
			return;
			
		pianos.ajax({
			pendingMessage: 'Computing',
			type: 'post',
			processData: false,
			contentType: 'application/json',
			url: params.intersectScript,
			data: JSON.stringify(post),
			success: function(data) {
				if (!data.success)
				{
					alert('Error: '+data.message);
					return;
				}
				
				
				setFinalQualityMap(data.ret, post, layerMapping);
			},
			error: function(xhr, status, error) {
				alert('HTTP error (code '+xhr.status+'): '+(error?error:'connection failed'));
			},
		});
	}
	
	var setFinalQualityMap = function(output, input, layerMapping)
	{
		var action = new Pianos4Action_Batch('Set final quality map');
		
		
		for (var i in output)
		{
			var view = pianos.getView(i);
			
			var lm = layerMapping[view.getType()];
			// Remove previous polygons
			var inputData = input[i];
			for (var j in inputData)
			{
				var areaID = inputData[j][0];
				var a = new Pianos4Action_DeleteAnnotation(view, areaID);
				action.append(a);
			}
			// Add new polygons
			var viewData = output[i];
			for (var j in viewData)
			{
				var layer = viewData[j];
				var polylayer = parseInt(j);
				
				var qualityID = lm[polylayer];
				
				for (var k in layer)
				{
					var polygon = layer[k];

					var area = new Pianos4Area(polygon.outer, getConstantColorById(qualityID, Pianos4Constants.quality[ansiNistQuality?'ansi_nist':'standard']), 'quality', qualityID, polygon.rings);
					var a = new Pianos4Action_CreateAnnotation(view, 'area', area);
					action.append(a);
				}
			}
			
		}
		
		pianos.exec(action);
	}
	
	var setPrimaryQuality = function(elem, quality)
	{
		primaryQuality = quality;
		
		var colors = $(elem).children('.pianos4_labelled_color');
		
		
		var measured = $(colors[primaryQuality]);
		var dims = [measured.outerWidth(true), measured.outerHeight(true)];
		var bkg = $(elem).find('.pianos4_labelled_color_bkg');
		var p = measured.position();
		
		bkg.width(dims[0]).height(dims[1]).css('top', p.top);
	}
}

Pianos4ToolRidge.createColorPicker = function(element, onclick, scale)
{
	// Color scale taken from Protovis (http://vis.stanford.edu/protovis/docs/color.html)
	var values;
	if (scale === 'quality_ansi_nist')
		values = Pianos4Constants.quality.ansi_nist;
	else if (scale === 'quality_std')
		values = Pianos4Constants.quality.standard;
	else if (scale === 'wcs')
		values = Pianos4Constants.details;
	else
	{
		values = [];
		var colors = ["#d62728", "#1f77b4", "#ff7f0e", "#2ca02c", "#9467bd", "#8c564b", "#e377c2", /*"#7f7f7f",*/ "#bcbd22", "#17becf" ];
		for (var i in colors)
			values.push({id:i, color: colors[i]});
	}
	
	for (var i in values)
	{
		var value = values[i];
		
		var icon = $('<span class="pianos4_tool_color ui-corner-all" style="background-color:'+value.color+'"><span /></span>');
		if (onclick)
			icon.click(onclick);
		
		// This is HTML5, and not XHTML 1.1, but who cares ?
		icon.attr('data-id', value.id);
		icon.attr('data-color', value.color);

		if (value.label)
		{
			icon.attr('title', value.label);
			var div = $('<div class="pianos4_labelled_color" />');
			div.append(icon);
			var span = $('<span />');
			var tokens = value.label.split(' ');
			for (var j in tokens) { span.append(tokens[j]).append('<br />'); }
			div.append(span);

			// Create a temporary, invisible div to calculate the height of the content.
			var tempHeightTester = $('<div class="pianos4_tools_options ui-widget ui-widget-content" style="visibility:hidden" />').appendTo($('body'));
			var tempHeightTesterInner = $('<div class="_content" />').appendTo(tempHeightTester);
			tempHeightTesterInner.append(div);
			var h = div.outerHeight(true) + 4;
			div.detach();
			tempHeightTester.remove();

			div.css('height',h+'px');
			$(element).append(div);
		}
		else
		{
			icon.attr('title', value.color);
			$(element).append(icon);
		}
	}
}


/* pianos4_ui.js */
/*
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/.
*/


// ----------------------------
// Shared UI classes
// ----------------------------

function Pianos4_FilterDialog(url, filters, onOK)
{
	
	
	this.show = function()
	{
		init();
		buildUI();
		setFilter(filters);
		
		pianos.dialog(main, {
			title: 'Results filter',
			width: 600,
			modal: true,
			buttons: {
				'Stop filtering': function() {
					$(this).dialog('close');
					onOK(null);
				},
				'Cancel': function() {
					$(this).dialog('close');
				},
				'OK': function() {
					var newFilter = getFilter();
					$(this).dialog('close');
					onOK(isFilterPassthrough(newFilter) ? null : newFilter);
				},
			},
			close: function() { $(this).remove(); },
		});
		var bpane = main.parents('.ui-dialog').find('.ui-dialog-buttonpane .ui-dialog-buttonset');
		bpane.css('width','100%');
		bpane.find('.ui-button:first').css('float','left');
		bpane.find(':not(.ui-button:first)').css('float','right');
		
	}
	
	this.getFilterAsString = function()
	{
		if (columns === null)
			init();
		return filterToString(filters);
	}
	
	this.isItemActive = function(item)
	{	
		if (columns === null)
			init();
		return !isColumnPassthrough(filters, item);
	}
	
	var self = this;
	var columns = null;
	var main = null;
	
	var init = function()
	{
		columns = {
			user: ['User', 'text'],
			group: ['Group', 'text'],
			progress: ['Progress', 'combo', null],
			analysis: ['Analysis', 'combo', null],
			comparison: ['Comparison', 'combo', null],
		};
		columns.progress[2] = {};
		for (var i in Pianos4Constants.acev.progress)
		{	
			var item = Pianos4Constants.acev.progress[i];
			columns.progress[2][item.id] = item.label;
		}
		columns.analysis[2] = {};
		for (var i in Pianos4Constants.conclusions.analysis.value)
		{	
			var item = Pianos4Constants.conclusions.analysis.value[i];
			columns.analysis[2][item.id] = item.label;
		}
		columns.comparison[2] = {};
		for (var i in Pianos4Constants.conclusions.comparison.decision_step1)
		{	
			var item = Pianos4Constants.conclusions.comparison.decision_step1[i];
			columns.comparison[2][item.id] = item.label;
		}
	}
	
	var filterToString = function(filter)
	{
		var rv = [];
		for (var i in columns)
		{
			var col = columns[i];
			if (i in filter)
			{
				var value = null;
				if (col[1] == 'text') {
					value = $.trim(filter[i]);
					if (value.length==0)
						value = null;
				} else if (col[1] == 'combo') {
					var index = parseInt(filter[i]);
					if (index >= 0)
					{
						
						value = col[2][index];
					}
				}
				if (value !== null)
					rv.push(i+'='+value);
			}
		}
		return rv.join(', ');
	}
	
	var filterToHTML = function(filter)
	{
		var text = filterToString(filter);
		if (text.length == 0)
			return $('<span />').css('font-style','italic').text('None');
		else
			return $('<span />').text(text);
	}
	
	var buildUI = function()
	{
		main = $('<div />')
			.append($('<div />').text('Current filter: ')
				.append($('<span />').addClass('cur_filter').append(filterToHTML(filters))))
			.append($('<div />').text('New filter: ')
				.append($('<span />').addClass('new_filter').append(filterToHTML(filters))));
				
		var id_base = 'pianos4_filter_opt_'+(new Date().getTime())
				
		var form = $('<form />')/*.attr('action','?action=export').attr('enctype','multipart/form-data').attr('method','post')*/.appendTo(main);
		
		var useTable = false;
		if (useTable)
		{
			var table = $('<table />').css('width','100%').css('font-size','80%');
			var head = $('<thead />').appendTo(table);
			var htr = $('<tr />').appendTo(head);
			var body = $('<tbody />').appendTo(table);
			var tr = $('<tr />').appendTo(body);
			var addHeader = function() {
				return $('<th />').appendTo(htr);
			};
			var addCell = function() {
				return $('<td />').appendTo(tr);
			};
			form.append(table);
		}
		else
		{	
			var wrapper = $('<div />').appendTo(form).css('font-size','80%');
			var lines = [];
			var curcell = 0;
			var addHeader = function() {
				var div = $('<div />').appendTo(wrapper).css('margin','0.5em 0 0.5em 0');
				lines.push(div);
				return $('<span />').appendTo(div).css('text-align','right').css('display','inline-block').css('width','10em').css('margin-right','1em');
			};
			var addCell = function() {
				
				var div = lines[curcell++];
				return $('<span />').appendTo(div);
			};
		}
		
		for (var i in columns)
			addHeader().text(columns[i][0]);
		
		for (var i in columns)
		{
			var col = columns[i];
			var cell = addCell();
			var element = null;
			if (col[1] == 'text')
			{
				element = $('<input />');
				cell.append(element);
			}
			else if (col[1] == 'combo')
			{
				element = $('<select />')
				var sel = element.appendTo(cell);
				$('<option />').attr('value',-1).html('<span style="font-style:italic">No filter</span>').appendTo(sel);
				for (var j in col[2])
					$('<option />').attr('value',j).text(col[2][j]).appendTo(sel);
			}
			
			element.attr('name', i).attr('id', id_base+'_'+i).on('change', function(e) {
				main.find('.new_filter').html('').append(filterToHTML(getFilter()));
			});
		}
	}
	
	var getFilter = function()
	{
		var data = main.find('form').serializeArray();
		
		var ret = {};
		for (var i in data)
		{
			var item = data[i];
			
			
			var col = columns[item.name];
			var v = null;
			if (col[1] == 'text')
				v = $.trim(item.value);
			else if (col[1] == 'combo')
				v = parseInt(item.value);
			
			ret[item.name] = v;
		}
		return ret;
	}
	
	var setFilter = function(filter)
	{
		for (var i in filter)
		{
			
			var col = columns[i];
			var elem = main.find('form [name='+i+']');
			elem.val(filter[i]);
		}
	}
	
	// Returns true if the given filter is useless (i.e. will
	// always match)
	var isFilterPassthrough = function(filter)
	{
		for (var i in filter)
		{
			if (!isColumnPassthrough(filter, i))
				return false;
		}
		return true;
	}
	var isColumnPassthrough = function(filter, i)
	{
		
		var col = columns[i];
		if (col[1] == 'text')
		{
			if (filter[i].length)
				return false;
		}
		else if (col[1] == 'combo')
		{
			if (filter[i] >= 0)
				return false;
		}
		else
		{
			
		}
		return true;
	}
}


/* pianos4_view.js */
/*
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).
		//
		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);
		
		
		
		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];
		/*
		//
		var pt = d3svg.node().createSVGPoint();
		pt.x = canvaspos[0];
		pt.y = canvaspos[1];
		var localPoint = pt.matrixTransform(d3svg.select('.pianos4_root').node().getScreenCTM());
		//
		//
		return [localPoint.x, localPoint.y];
		*/
		return ret;
	}
	
	this.addMinutia = function(minutia)
	{
		
		annotations.minutiae[minutia.id] = minutia;
		allAnnotations[minutia.id] = ['minutia', minutia];
		
		onAnnotationAdded(minutia);
		
		drawMinutia(minutia);			
	}
	
	this.removeMinutia = function(minutia)
	{
		
		delete annotations.minutiae[minutia.id];
		delete allAnnotations[minutia.id];
		
		clearMinutia(minutia);
		
		onAnnotationRemoved(minutia);
	}

	this.updateMinutiaPairing = function(minutia)
	{
		

		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+'.';
		// 
			
		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;
			}
			
			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);
		
		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)
	{
		
		annotations.ridges[ridge.id] = ridge;
		allAnnotations[ridge.id] = ['ridge', ridge];
		
		onAnnotationAdded(ridge);
		
		drawRidge(ridge);	
	}
	
	this.removeRidge = function(ridge)
	{
		
		delete annotations.ridges[ridge.id];
		delete allAnnotations[ridge.id];
		
		clearRidge(ridge);
		
		onAnnotationRemoved(ridge);
	}

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

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

	this.updateAnnotation = function(annotation, values)
	{
		
		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.
			
			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)
	{
		
		var dict = annotations[annotGroupByContent[type]];
		
		return dict[id];
	}
	
	this.getAllAnnotations = function()
	{
		return allAnnotations;
	}
	
	this.getAnnotationById = function(id)
	{
		
		return allAnnotations[id];
	}
	
	this.getAnnotationType = function(id)
	{
		
		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)
	{
		
		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);
					
					ret.node = redir.node();
				}
				if (ret) 
					break;
			}
			
				
			if (n.parentNode)
				n = n.parentNode;
			else
				n = null;
		}
		if (ret)
		{
			
			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];

				
				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);
				
				
				if (type == 'minutia')
				{
					
					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);
		
		
		
		node.select('._gizmo').remove();
		
		if (type == 'minutia') {
			
			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()
	{
		
		
		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()
			{
				// 
				// 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)
	{
		

		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);
				
				var gizmo = defs.select('#'+items[i]+suffix+'_gizmo');
				

				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)
	{
		
		$(['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)
	{
		
		
		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)
	{
		
		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)
	{
		
		
		if (capturingTool.onMouseMove(self.getCanvasPos([event.clientX, event.clientY]), event.target, event))
			releaseMouse();
	}
	
	var onMouseUp = function(event, ui)
	{
		

		if (capturingTool.onMouseUp(self.getCanvasPos([event.clientX, event.clientY]), event.target, event))
			releaseMouse();
	}
	
	var captureMouse = function(tool) 
	{
		
		capturingTool = tool;
		capturingTool.isBusy = true;
		
		

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

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

		

		// 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);
				
					
				// 
				// 
				// 
				// //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)
		{
			//
			//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);
		}

		
		
		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);
		//
		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)
	{
		
		var parent = null;
		if (params.splitByUser)
		{
			
			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];
		}
		
		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);
					
					// '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);
		
		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);
		
		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);
		
		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);
		
		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:  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:  break;
		}
	}
	
	var drawSegments = function()
	{
		
		
		var base = self.isMark() ? 1 : 3;
		for (var i in segments)
		{
			var s = segments[i];
			
			var from = s[base];
			var to = s[base+1];
			
			
			
			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()
	{
		
		var ctx = h5Canvas.get(0).getContext('2d');
		for (var i in h5CanvasCommands)
		{	
			var command = h5CanvasCommands[i];
			drawH5CanvasCommand(command, ctx);
		}
	}
	
	var drawH5CanvasCommand = function(command, ctx)
	{
		

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


