/*
 * PiAnoS (Picture Annotation System), A fingerprint examiner training tool
 * Copyright (C) 2011-2021 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';
	assert(params.type == 'ridge' || params.type == 'area' || params.type == 'quality' || params.type == 'wcs');
	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)
	{
		assert(!view);
		view = aView;
	}
	
	this.unbind = function()
	{
		assert(view);
		view = null;
	}
	
	this.activate = function()
	{
	}
	
	this.deactivate = function()
	{
	}
	
	this.onMouseDown = function(pos)
	{
	    var rotationData = rotationView[ view.getType() ];
	    var pos_r = rotateAround( pos, rotationData, false );
	    
		if (points_rotated.length == 0)
		{
			// A first click creates the ridge/area
			
			assert(!path && !line);
			
			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 = points_rotated.slice(-1)[0];
			var closeToLastPoint = getDistance(lastPos, pos_r) < params.closeRidgeThreshold;
			var closeToFirstPoint = params.type == 'area' && points_rotated.length>0 && getDistance(points_rotated[0], pos_r) < 2.0*params.closeAreaThreshold; 
			if (closeToLastPoint || closeToFirstPoint)
			{
				line.remove();
				line = null;
				path.remove();
				path = null;
				
				var nextColor = false;
				
				if (params.type == 'ridge' && points_rotated.length >= 2)
				{
					var action = new Pianos4Action_CreateAnnotation(view, 'ridge', new Pianos4Ridge(points_rotated, color, width, opacity));
					pianos.exec(action);
					nextColor = true;
				}
				else if ((params.type == 'area' || params.type == 'quality' || params.type == 'wcs') && points_rotated.length >= 3)
				{
					var area;
					if (params.type == 'quality')
						area = new Pianos4Area(points_rotated, color, 'quality', getConstantIdByColor(color, Pianos4Constants.quality[ansiNistQuality?'ansi_nist':'standard']));
					else if (params.type == 'wcs')
						area = new Pianos4Area(points_rotated, color, 'wcs', getConstantIdByColor(color, Pianos4Constants.details));
					else
						area = new Pianos4Area(points_rotated, 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_rotated = [];
				points_live = [];
				return true;
			}
		}
		
		line
			.attr('x1', pos[0])
			.attr('y1', pos[1])
			.attr('x2', pos[0])
			.attr('y2', pos[1])
		;
		points_rotated.push(pos_r);
		points_live.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');
				
				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_rotated = [];
		points_live = [];
	}
	
	this.isQualityAnsiNist = function()
	{
		assert(params.type == 'quality');
		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_rotated = [];
	var points_live = [];
	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_live.length >= 2)
		{
			var def = 'M ';
			for (var i in points_live)
			{
				if (i>0)
					def += ' L';
				def += ' '+points_live[i][0]+' '+points_live[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)
	{
		assert(value != ansiNistQuality);
		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];
				assert('value' in area);
				assert(area.value in convMap);
				
				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() {
				assert(!firstRun); // 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];
			assert('value' in area);

			// '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()
	{
		assert(params.intersectScript);
		
		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;
				}
				
				log(data.ret);
				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');
		
		log(layerMapping);
		for (var i in output)
		{
			var view = pianos.getView(i);
			assert(view.getType() in layerMapping);
			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);
				assert(polylayer in lm);
				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');
		assert(primaryQuality >= 0 && primaryQuality < colors.length);
		
		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);
		}
	}
}
