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

/**
 * 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;
    assert( params.title === null || typeof params.title == 'string' || ( typeof params.title == 'object' && 'length' in params.title && params.title.length == 3 ) );
    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.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.startsWithConclusionOpen == 'undefined' )
        params.startsWithConclusionOpen = false;
    
    // Skip the quality checks and set the default values
    if( !params.skip_mendatory_fields_and_set_default )
        params.skip_mendatory_fields_and_set_default = false
        
    if( params.skip_mendatory_fields_and_set_default )
    {
        params.skip_quality_and_minutiae_checks = true;
        params.skip_case_notes_check = true;
        params.skip_conclusion_check = true;
    } else {
        params.skip_quality_and_minutiae_checks = false;
        params.skip_case_notes_check = false;
        params.skip_conclusion_check = false;
    }
    
    if( params.skip_quality_and_minutiae_checks__manualconfig === true || params.skip_quality_and_minutiae_checks__manualconfig === false )
        params.skip_quality_and_minutiae_checks = params.skip_quality_and_minutiae_checks__manualconfig;
    
    // 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',
            'GC': 'GhostCursor',
            'TPSlines': 'TPS delta lines',
            'ruler': 'Synthetic ruler'
        };
        for( var i in defaultLayerNames )
        {
            if( !( i in params.layerNames ) )
                params.layerNames[ i ] = defaultLayerNames[ i ];
        }
        
        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,
                markCanHaveCenter: params.markCanHaveCenter
            } ),
        ];
        
        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( typeof( TPS_generate ) !== "undefined" && params.phase != 'analysis' )
            activeTools.push( new Pianos4ToolTPS() );
        
        activeTools.push( new Pianos4ToolMeasure() );
        
        if( typeof( Pianos4ToolImageEditor ) !== "undefined" )
            activeTools.push( new Pianos4ToolImageEditor() );
        
        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 );
        
        // Preload some images from the analysis view
        if( params.phase === "analysis" )
        {
            $( document ).ready( function()
            {
                setTimeout( function()
                {
                    preloader( 'css/location/main.png' );
                    preloader( params.print.href, 120, 15 );
                }, 100 );
            } );
        }
    }

    // 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 )
    {
        assert( params.loadResults );
        
        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
                {
                    toastr.error( data.exception, 'Error: could not load the results', { timeout: 5000 } );
                }
            },
        } );
    }

    this.getServerData = function()
    {
        return serverData;
    }

    this.save = function( submit, force )
    {
        assert( params.saveResults );
        
        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';
            
            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'

            if( ! params.skip_conclusion_check )
            {
                if( !conclusions.mandatoryOK )
                {
                	var uglyMapping;
                    if( params.phase == 'analysis' )
                    {
                        uglyMapping = {
                            'mark-degradation': '1',
                            'mark-pfsp': '2',
                            'mark-gp': '3',
                            'mark-details': '4',
                            'mark-suitability-approach': '5b',
                            'mark-value': '5',
                        };
                    } 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' ) );
                            }
                        }
                    }

                    assert( missing.length > 0 );
                    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
                {
                    if( !params.skip_quality_and_minutiae_checks )
                    {
                        var as = self.getAnnotationsSummary();
                        log( as );
                        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 && ! params.skip_case_notes_check )
            {
                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;
            }
        }
        
        assert( !submit || params.submitResults );
        
        var data = params.v3 ? self.getDataV3( submit ) : self.getData();
        
        if( params.skip_conclusion_check )
        {
        	if( data[ 'observations' ] === undefined )
        		data[ 'observations' ] = {};
        	
        	if( params.phase == 'analysis' )
        	{
        		if( data[ 'observations' ][ 'mark-degradation' ] === undefined )
            		data[ 'observations' ][ 'mark-degradation' ] = null;
            	
        		if( data[ 'observations' ][ 'mark-gp' ] === undefined )
            		data[ 'observations' ][ 'mark-gp' ] = null;
        		
        		if( data[ 'observations' ][ 'mark-pfsp' ] === undefined )
            		data[ 'observations' ][ 'mark-pfsp' ] = null;
        		
            	if( data[ 'observations' ][ 'mark-details' ] === undefined )
            		data[ 'observations' ][ 'mark-details' ] = null;
            	
            	if( data[ 'observations' ][ 'mark-suitability-approach' ] === undefined )
            		data[ 'observations' ][ 'mark-suitability-approach' ] = -1;
            	
            	if( data[ 'observations' ][ 'mark-value' ] === undefined )
            		data[ 'observations' ][ 'mark-value' ] = null;
            	
            	if( data[ 'observations' ][ 'conclusion' ] === undefined )
	        		data[ 'observations' ][ 'conclusion' ] = null;
            	
        	} else if( params.phase == 'comparison'  )
        	{
        		if( data[ 'observations' ][ 'comparison-decision' ] === undefined )
	        		data[ 'observations' ][ 'comparison-decision' ] = null;
	        	
	        	if( data[ 'observations' ][ 'comparison-l3details' ] === undefined )
	        		data[ 'observations' ][ 'comparison-l3details' ] = null;
	        	
	        	if( data[ 'observations' ][ 'comparison-qualquant' ] === undefined )
	        		data[ 'observations' ][ 'comparison-qualquant' ] = null;
	        	
	        	if( data[ 'observations' ][ 'conclusion' ] === undefined )
	        		data[ 'observations' ][ 'conclusion' ] = null;
        	}
        }
        
        self.ajax( {
            pendingMessage: 'Saving',
            type: 'post',
            processData: false,
            contentType: 'application/json',
            url: submit ? params.submitResults : params.saveResults,
            data: JSON.stringify( data ),
            success: function( data )
            {
                if( ( 'status' in data && data.status != 'ok' ) || ( 'error' in data && data.error ) )
                    toastr.error( data.message, 'Saving error', { timeout: 5000 } );
                else
                {
                    toastr.success( '', 'Saved !', { timeOut: 1000 } );
                    lastSaveData.stamp = new Date().getTime();
                    lastSaveData.stackPtr = stackPtr;
                    if( submit )
                        if( params.phase == "comparison" )
                            window.location.href = '?';
                        else
                            window.location.href = '?action=take&id=' + params.exid;
                }
            },
        } );
    }

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

            updateUI();
        }

        o.error = function( xhr, status, error )
        {
            assert( operations.length > 0 && xhr.pianos4_id in operations );
            delete operations[ xhr.pianos4_id ];
            --operations.length;
            
            if( options.error )
                options.error( xhr, status, error )

            updateUI();
        }

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

        $.ajax( o );
    }

    this.enterModal = function()
    {
        assert( doingModal >= 0 );
        doingModal++;
        
        updateUI();
    }

    this.leaveModal = function()
    {
        assert( doingModal > 0 );
        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 )
    {
        assert( name in tools );
        return tools[ name ];
    }

    this.getView = function( name )
    {
        assert( name == 'mark' || name == 'print' );
        if( name == 'mark' )
        {
            assert( mark );
            return mark;
        } else if( name == 'print' )
        {
            assert( print );
            return print;
        }
    }

    this.getViews = function()
    {
        var r = [];
        if( mark )
            r.push( mark );
        if( print )
            r.push( print );
        return r;
    }
    
    this.getViewsdict = function()
    {
        return ret = {
            mark: mark,
            print: print,
        };
    }
    
    this.exec = function( action )
    {
        if( Array.isArray( action ) )
        {
            for( var i = 0; i < action.length; i++ )
                this.exec( action[ i ] );
        
        } else {
            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 )
    {
        assert( stackPtr >= 0 && stackPtr < stack.length );
        var action = stack[ stackPtr ];
        action.undo();
        --stackPtr;
        
        if( !noUpdateUI )
            updateUI();
    }

    this.redo = function( noUpdateUI )
    {
        assert( stackPtr >= -1 && stackPtr < stack.length - 1 );
        ++stackPtr;
        var action = stack[ stackPtr ];
        action.redo();
        
        if( !noUpdateUI )
            updateUI();
    }
    
    this.getParams = function()
    {
    	return params;
    }
    
    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 ];
            assert( !( assignment.userid in users ) );
            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 ];
                        assert( m.type in params.minutiaeTypes );
                        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.getCaseName = function()
	{
		return params.caseName;
	}
	
	this.getCaseID = function()
	{
		return params.exid;
	}
	
	this.getMarkInformation = function()
	{
		return params.mark;
	}
	
	this.getPrintInformation = function()
	{
		return params.print;
	}
	
    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;
                }
                assert( dataShown && $.isArray( dataShown ) );
                
                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 ];
                    
                    if( ! ( assignment.user.id in users ) )
                    {
                        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 );
                    } else {
                        toastr.warning( "Duplicate data for " + assignment.user.name, "Data not loaded" );
                    }
                }
                
                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 ];
            
            if( data[ field[ 2 ] ] == null )
            {
                continue;
            } else {
                switch( field[ 0 ] )
                {
                    case 'string':
                        var valueToSet = field[ 2 ] in data ? data[ field[ 2 ] ] : '';
                        field[ 1 ].val( valueToSet );
                        assert( 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.
                        assert( field[ 1 ].val() == valueToSet );
                        break;
                    case 'data':
                        if( field[ 2 ] in data )
                            field[ 1 ].data( 'pianos4', data[ field[ 2 ] ] );
                        else
                            field[ 1 ].removeData( 'pianos4' );
                        break;
                    default:
                        assert( false );
                        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();
            // log('Selection changed to');
            // log(sel);
            
            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' )
                            {
                                assert( 'quality' in tools );
                                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 )
                    {
                        assert( 'userid' in annotation && ( annotation.userid in users || annotation.userid == params.user.id ) );
                        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' )
                                                {
                                                    assert( 'quality' in tools );
                                                    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
                                {
                                    assert( false );
                                }
                            } 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.setMinutiaMarkerTransparency = function( factor )
    {
    	if( mark )
    		mark.setMinutiaMarkerTransparency( factor );
    	if( print )
    		print.setMinutiaMarkerTransparency( factor );
    }
    
    this.setPairingMarkerSize = function( factor )
    {
        if( mark )
            mark.setPairingMarkerSize( factor );
        if( print )
            print.setPairingMarkerSize( 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
        {
            assert( id in users );
            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 )
    {
        assert( phase in params.phases );
        
        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 ];
                assert( atype in data );
                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;
        assert( toolname && toolname in tools );
        
        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 )
    {
        assert( params.user.tutor );
        var res = {};
        
        panes.results.find( 'table input[type=checkbox]' ).each( function()
        {
            var cb = $( this );
            var uid = cb.data( 'pianos4.userid' );
            assert( uid && parseInt( uid ) > 0 );
            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,
            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,
                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" />' );
        
        /*
         *	Misc tools
         */
        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 ] );
        
        /*
         *	Annotation slider (minutiae and paring)
         */
        
        if ( params.show_marker_size_slider )
        {
			var sliderMin = params.makerSizeMin;
			var sliderMax = params.makerSizeMax;
			var nbsteps = 50;

			var slider = createToolSlider( 'Marker size',
			{
				min : 0,
				max : nbsteps,
				value : nbsteps / 2,
				onSlide : function( value )
				{
					markerSize = value / nbsteps * ( sliderMax - sliderMin ) + sliderMin;
					pianos.setMinutiaMarkerSize( markerSize );
					pianos.setPairingMarkerSize( markerSize );
				},
			}, false );

			var markersizeslider = $( '<div />' )
				.css( 'font-size', '0.7em' )
				.append( slider );

			toolboxInner.append( markersizeslider );
		}
        
        /*
		 *	Annotation transparency slider (minutiae and paring)
		 */
        
        if( params.show_marker_opacity_slider )
        {
			var slider = createToolSlider( 'Marker opacity',
			{
				min : 0,
				max : 100,
				value : 100,
				onSlide : function( value )
				{
					pianos.setMinutiaMarkerTransparency( value / 100 );
				},
			}, false );

			var markeropacityslider = $( '<div />' )
				.css( 'font-size', '0.7em' )
				.append( slider );

			toolboxInner.append( markeropacityslider );
		}
                
        /*
         *	Preparation of the toolbox variable and return
         */
        
        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' ) );
        
        rotateUsersCheck = false;
        var rotateUsers = function()
        {
            if( rotateUsersCheck )
            {
                $( '#userscycle > span > span' )
                    .addClass( "ui-icon-cancel" )
                    .removeClass( "ui-icon-clock" );
                
                onSelectNextUserResults();
                setTimeout( rotateUsers, 1000 );
            } else {
                $( '#userscycle > span > span' )
                    .addClass( "ui-icon-clock" )
                    .removeClass( "ui-icon-cancel" );
            }
        }
        
        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-clock', 'Automatically review next user', function()
                        {
                            rotateUsersCheck = ! rotateUsersCheck;
                            rotateUsers();
                        } ).attr( 'id', 'userscycle' )
                    )
                    .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(
                        createBtn( 'ui-icon-note', 'PFSP', function()
                        {
                            showUserData();
                        } )
                    )
                    .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', 'GC', 'TPSlines', 'ruler'
        ];
        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' );
            array_remove( l, 'GC' );
            array_remove( l, 'TPSlines' );
        }
        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 )
            {
                assert( item in params.layerNames );
                
                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();
        } ) );
        
        buttons.push( createButton( 'reset_view', 'Reset interface', 'Reset the rotation angle for the mark and the print' )
            .addClass( 'pianos4_toolbar_button_spaced' )
            .bind( 'click', function()
            {
                rotationView.mark.theta = 0;
                rotationView.print.theta = 0;
                
                $( '#rotationslider_mark' ).slider( "value", 0 );
                $( '#rotationslider_print' ).slider( "value", 0 );
                
                pianos.getViews().forEach( function( v )
                {
                    v.updateRotation();
                } );
                
                self.setZoomFit();
                
                if( params.imageeditor )
                    image_editor_parameters_reset();
            } )
        );
        
        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.user.tutor && params.user.switchedUser )
        {
            buttons.push( createButton( 'switchback', 'Switch back to tutor', 'Go back as tutor' )
                .css( 'margin-left', '30px' )
                .css( 'margin-right', '30px' )
                .bind( 'click', function()
                {
                    window.location.href = '?action=switchUser&exid=' + params.exid + '&step=' + params.phase
                } )
            );
        }
        
        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.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 />' )
                                    .css( 'margin-left', '0.5em' )
                                    .text( toggleConclusionsTexts[ 0 ] )
                            )
                            .click( function( e )
                            {
                                e.preventDefault();
                                toggleConclusion();
                                $( this ).blur();
                            } )
                    )
            );
        
        if( !params.user.tutor )
        {
            // "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' )
                        )
                );
                
                // Palm and finger segment positions
                conclusions.append(
                    $( '<div />' )
                        .css( 'margin-top', '1em' )
                        .append(
                            $( '<div />' )
                                .css( 'font-weight', 'bold' )
                                .text( '2. Palm and finger segment positions:' )
                        )
                        .append(
                            $( '<a />' )
                                .attr( 'href', '#' )
                                .text( 'Click here to specify ...' )
                                .click( function( e )
                                {
                                    e.preventDefault();
                                    var content = $( this ).next( 'div' );
                                    var initData = content.data( 'pianos4' );
                                    
                                    showPalmFingerSegementPositionDialog(
                                        'Palm and finger segment positions',
                                        Pianos4Constants.conclusions.analysis.palm_finger_segment_position,
                                        function( data )
                                        {
                                            content.data( 'pianos4', data );
                                            onConclusionUpdated();
                                        },
                                        initData,
                                        false
                                    );
                                } )
                        )
                        .append(
                            $( '<div />' )
                                .attr( 'id', 'pianos4_conclusion_analysis_palm_finger_segment_positions' )
                                .addClass( 'pianos4_conclusion_value_string' )
                        )
                );
                
                // General pattern
                conclusions.append(
                    $( '<div />' )
                        .css( 'margin-top', '1em' )
                        .append(
                            $( '<div />' )
                                .css( 'font-weight', 'bold' )
                                .text( '3. 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( '4. 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( '5. Suitability of the latent mark :' )
                        )
                        .append( menu )
                );
                
            } else if( params.phase == 'comparison' )
            {
                var menu = $( '<select />' )
                    .attr( 'id', 'pianos4_conclusion_comparison_decision' )
                    .change( function( e )
                    {
                        onConclusionUpdated();
                    } );
                
                $( '<option />' )
                    .attr( 'value', '-1' )
                    .css( 'font-style', 'italic' )
                    .text( 'Please choose' )
                    .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' )
                    .css( 'font-style', 'italic' )
                    .text( 'Please choose' )
                    .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();
                    } )
                    .append(
                        $( '<option />' )
                            .attr( 'value', '-1' )
                            .css( 'font-style', 'italic' )
                            .text( 'Please choose' )
                    )
                    .append(
                        $( '<option />' )
                            .attr( 'value', 2 )
                            .text( 'Yes' )
                    )
                    .append(
                        $( '<option />' )
                            .attr( 'value', 1 )
                            .text( 'Possibly' )
                    )
                    .append(
                        $( '<option />' )
                            .attr( 'value', 0 )
                            .text( 'No' )
                    );
                
                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();
                    } )
                    .append(
                        $( '<option />' )
                            .attr( 'value', '-1' )
                            .text( 'Please choose' )
                            .css( 'font-style', 'italic' )
                    )
                    .append(
                        $( '<option />' )
                            .attr( 'value', 1 )
                            .text( 'Yes' )
                    )
                    .append(
                        $( '<option />' )
                            .attr( 'value', 0 )
                            .text( 'No' )
                    );
                
                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 )
                );
            }
            
            // Case notes label
            var caseNotesQuestionIndex = params.phase == 'analysis' ? 6 : 3;
            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();
                                } )
                        )
                );
        } else {
        }
        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', 3 ).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, 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 );
                    assert( from == 1 && to > from );
                    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
                    assert( typeof d.progress.status == "number" && d.progress.status >= 0 && d.progress.status < agg.progress.length );
                    agg.progress[ d.progress.status ]++;
                    
                    // Mark value
                    if( typeof d.analysis.value == "number" )
                    {
                        assert( d.analysis.value >= 0 && d.analysis.value < agg.mark_value.length );
                        agg.mark_value[ d.analysis.value ]++;
                    } else
                        unset_mark_values++;
                    
                    // Final decision
                    if( typeof d.comparison.decision == "number" )
                    {
                        var d1 = d.comparison.decision;
                        assert( d1 >= 0 && d1 <= 2 );
                        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;
                            assert( typeof d2 == "number" && d2 >= 0 && d2 <= 2 );
                            dec = 2;
                            if( d2 == 2 )
                                dec--;
                            else
                                dec += d2;
                        } else if( d1 == 1 ) // identification
                        {
                            dec = 4;
                        }
                        assert( typeof dec == "number" && dec >= 0 && dec < agg.decision.length );
                        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 )
    {
        log( 'Users results visibility change, ' + numChecked + ' checked' );
        if( tutorDataReady )
            updateTutorGraphs();
    }

    var onTutorDataAvailable = function( what, data )
    {
        log( 'Tutor data available: ' + what );
        assert( !( what in tutorData ) );
        tutorData[ what ] = data;
        assert( Object.keys( tutorData ).length <= 2 );
        
        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;
                    assert( !( uid in idx ) );
                    idx[ uid ] = d;
                }
                return idx;
            }
            tutorData.results = indexData( tutorData.results );
            tutorData.graphs = indexData( tutorData.graphs );
            
            updateTutorGraphs();
        }
    }

    var updateTutorGraphs = function()
    {
        log( 'Updating tutors graphs' );
        assert( tutorDataReady && drawTutorSVG );
        var vis = self.getUsersResultsVisibility();
        var checked = [];
        var numUsers = 0;
        for( var uid in vis )
        {
            assert( uid in tutorData[ 'results' ] && uid in tutorData[ 'graphs' ] );
            numUsers++;
            var visible = vis[ uid ];
            var results = tutorData[ 'results' ][ uid ];
            var conclusions = tutorData[ 'graphs' ][ uid ];
            if( visible )
                checked.push( uid );
        }
        log( 'num users: ' + numUsers + ', checked:' + checked.length );
        
        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 ];
            assert( params.phase in conclusions );
            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 );
        assert( box.size() == 1 );
        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',
            'v': 'edit',
            'h': 'pan',
            'd': 'TPS'
        };
        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 ...
        
        self.registerHandler( [
            'keydown', 'shift+left'
   		], function()
        {
        	if( params.title[ 0 ][ 1 ] != undefined )
        	{
				url = params.title[0][1]
				if (params.title[0].length == 2)
					url += '&step=' + params.phase

				window.location.href = url;
			}
        } );
        
        self.registerHandler( [
            'keydown', 'shift+right'
        ], function()
        {
        	if( params.title[ 0 ][ 1 ] != undefined )
        	{
				url = params.title[2][1]
				if (params.title[2].length == 2)
					url += '&step=' + params.phase

				window.location.href = url;
			}
        } );
        
        self.registerHandler( [
            'keydown', 's'
   		], function()
        {
        	self.save();
        } );
        
        $( document ).keydown( function( e )
        {
            if( e.keyCode == 83 && e.ctrlKey )
            {
                e.preventDefault();
                pianos.save();
            }
        } );
    }

    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' );
        // log('Available for slider: '+avail);
        
        // 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 )
        {
            assert( item in panes );
            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 )
            {
                assert( !( item in sizes ) );
                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 )
            {
                assert( !( item in sizes ) );
                var prop = -value / sumOfProps;
                var height = prop * availH;
                sizes[ item ] = height;
                heightUsed += height;
            }
        }
        availH -= heightUsed;
        
        // assert(availH <= 0.0); // 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 )
    {
        assert( toolname in tools );
        
        log( 'activating tool ' + 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' );
            
            if( tool.afterLoaded )
                tool.afterLoaded();
            
            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();
        assert( wrapper.hasClass( 'ui-layout-pane-docked' ) || wrapper.hasClass( 'pianos4_floating_pane' ) );
        
        if( wrapper.hasClass( 'ui-layout-pane-docked' ) )
        {
            assert( wrapper.hasClass( 'ui-layout-' + where ) );
            
            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
        {
            assert( wrapper.hasClass( 'pianos4_floating_pane' ) );
            
            var p = $( '#' + params.divid + ' .ui-layout-' + where );
            assert( p.hasClass( 'ui-layout-pane-floating' ) );
            
            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 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' );
                assert( data && 'segments' in data && 'pairing' in data );
                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();
                        assert( idx in mtypes );
                        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 )
                            {
                                assert( a.userid in visibility );
                                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' );
        assert( me && me.numChecked >= 0 && me.numChecked <= me.numUsers );
        assert( uid in me.users );
        
        var muid = me.users[ uid ];
        if( muid.status != on )
        {
            assert( uid in users );
            if( mark )
                mark.setUserResultsVisible( uid, on );
            if( print )
                print.setUserResultsVisible( uid, on );
            
            muid.status = on;
            me.numChecked = on ? me.numChecked + 1 : me.numChecked - 1;
            assert( me.numChecked >= 0 && me.numChecked <= me.numUsers );
            
            panes.results.find( '.table_header_options>input[type=checkbox]' ).prop( 'indeterminate', me.numChecked != 0 && me.numChecked != me.numUsers );
        } else
        {
            log( 'notice: redundant call to toggleUserResults(' + uid + ',' + on + ')' );
        }
        
        if( !silenceEvent )
            onUsersResultsToggled( me.numChecked );
    }

    var toggleAllResults = function( on )
    {
        var me = panes.results.data( 'pianos4.me' );
        assert( users && me && me.numChecked >= 0 && me.numChecked <= me.numUsers );
        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 )
    {
        assert( params.user.tutor );
        
        // "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 switchto = $( '<td />' );
                switchto.append(
                    $( '<a />' )
                        .attr( 'href', '?action=switchUser&user=' + user.id + '&exid=' + params.exid + '&step=' + params.phase )
                        .append( '<span class="ui-icon ui-icon-arrowreturnthick-1-e ui-inline-icon" />' )
                );
                tr.append( switchto );
                
                
                var table = panes.results.find( 'table>tbody' );
                table.append( tr );
                me.users[ uid ].dom = cb;
            } )( i );
        
        onUsersResultsToggled( me.numChecked );
    }
    
    var showUserData = function()
    {
        var complete = function( data )
        {
            var dlgopts = {
                autoOpen: true,
                modal: true,
                width: 600,
                title: 'All user data',
                close: function()
                {
                    $( this ).remove();
                },
                buttons: {
                    'Close': function()
                    {
                        $( this ).dialog( 'close' );
                    }
                },
            };
            
            var div = $( '<div />' );
            var id_base = 'pianos4_resultdlg_opt_' + ( new Date().getTime() );
            
            div.qtable( {
                data: data.ret,
                columnMode: 'mapping',
                columns: [
                    {
                        title: 'User',
                        render: function( data, i, j, cell )
                        {
                            cell.append( 
                                $( '<span />' )
                                    .text( data[ i ].uname )
                            );
                        },
                    }, {
                        title: 'PFSP',
                        render: function( data, i, j, cell )
                        {
                            cell.append( 
                                $( '<span />' )
                                    .attr( 'id', 'userPFSP_' + data[ i ].uid )
                                    .text( 'Loading...' )
                                    .css( 'text-decoration', 'underline' )
                                    .css( 'cursor', 'pointer' )
                                    .bind( 'click', function(){
                                        showPFSPUser( params.exid, data[ i ].uid );
                                    } )
                            );
                            
                            self.ajax( {
                                pendingMessage: 'Loading conclusions',
                                url: '?action=rpc&method=getPFSPUser',
                                data: {
                                    exid: params.exid,
                                    uid: data[ i ].uid
                                },
                                dataType: 'json',
                                success: function( d )
                                {
                                    if( ! d.error )
                                    {
                                        if( d.ret.length == 0 || d.ret[ 0 ].pfsp === 'null' )
                                        {
                                            v = 'None';
                                        } else {
                                            v = d.ret[ 0 ].pfsp;
                                            v = v.replace( /[{}""\[\]]/g, '' ).split( "," );
                                            v = v.length;
                                            
                                            if( v == 1 )
                                                v = v + " zone";
                                            else if( v > 1 )
                                                v = v + " zones";
                                            else
                                                v = "None";
                                        }
                                        
                                        $( '#userPFSP_' + data[ i ].uid ).text( v );
                                    } else {
                                        toastr.error( "Server error" );
                                        $( '#userPFSP_' + data[ i ].uid ).text( 'Error' );
                                    }
                                },
                                error: function()
                                {
                                    toastr.error( "Network error" );
                                    $( '#userPFSP_' + data[ i ].uid ).text( 'Error' );
                                }
                            } );
                            
                        },
                    }
                ]
            } );
            
            var dlg = $( '<div />' )
                .addClass( 'pianos4_dialog' )
                .css( 'max-height', $( window ).height() - 140 )
                .append( div );
            
            self.dialog( dlg, dlgopts );
        }
        
        self.ajax( {
            pendingMessage: 'Loading conclusions',
            url: '?action=rpc&method=getExerciseAssignmentsUsers',
            data: {
                exid: params.exid
            },
            dataType: 'json',
            success: function( data )
            {
                complete( data );
            },
            error: function()
            {
                toastr.error( "Network error" );
            }
        } );
    } 
    
    var showPFSPUser = function( exid, userid )
    {
        self.ajax( {
            pendingMessage: 'Loading PFSP',
            url: '?action=rpc&method=getPFSP',
            data: {
                exid: exid,
                userid: userid
            },
            dataType: 'json',
            success: function( data )
            {
                if( ! data.error )
                {
                    if( data.ret.length === 0 )
                    {
                        toastr.info( "No data" );
                    } else {
                        var d = data.ret[ 0 ].value;
                        d = d.replace( /[{}""\[\]]/g, '' ).split( "," );
                        showPalmFingerSegementPositionDialog(
                            'Palm and finger segment positions',
                            Pianos4Constants.conclusions.analysis.palm_finger_segment_position,
                            null,
                            d,
                            true
                        );
                    }
                } else {
                    toastr.error( "Server error" );
                }
            },
            error: function()
            {
                toastr.error( "Network error" );
            }
        } );
    }
    
    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 );
                assert( $( this ).data( 'pianos4.userid' ) > 0 );
                toggleUserResults( $( this ).data( 'pianos4.userid' ), false );
            } );
        }
        if( next )
        {
            next.each( function()
            {
                $( this ).prop( 'checked', true );
                assert( $( this ).data( 'pianos4.userid' ) > 0 );
                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_palm_finger_segment_positions' ), 'mark-pfsp'
            ] );
            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'
            ] );
        }
        
        assert( ( function()
        {
            var ok = true;
            $.each( fields, function()
            {
                if( this[ 1 ].length != 1 )
                    ok = false;
            } );
            return ok;
        } )() );
        
        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:
                    assert( false );
                    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 )
                {
                    assert( conclusions.length > 0 );
                    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++;
        }
        
        assert( conclusions.length <= fields.length );
        
        conclusions.isOK = conclusions.length == fields.length;
        conclusions.mandatoryOK = numMandatory == numMandatoryAnswered;
        
        if( returnFields )
            return [
                conclusions, fields
            ];
        else
            return conclusions;
    }

    var onConclusionUpdated = function( noUndoRedo )
    {
        log( 'onConclusionUpdated' );
        if( params.phase != 'analysis' && params.phase != 'comparison' )
            return;
        
        var stuff = getConclusions( true );
        var conclusions = stuff[ 0 ];
        var fields = stuff[ 1 ];
        
        if( !noUndoRedo )
        {
            assert( previousConclusions );
            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-pfsp' in conclusions )
                setConclusionString( $( '#pianos4_conclusion_analysis_palm_finger_segment_positions' ), conclusions[ 'mark-pfsp' ] );
            else
                $( '#pianos4_conclusion_analysis_palm_finger_segment_positions' ).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 );
                        assert( conclusions.length > 0 );
                        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();
            }
        }
        
        var color;
        if( params.skip_mendatory_fields_and_set_default )
            color = '#004cff';
        else 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( ! Array.isArray( values ) )
        {
            destination.append( formatFunc( values ) );
        } else 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 );
        }
        
        assert( initValue.length == items.length );
        
        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 ];
            assert( item.length >= 3 );
            
            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 showPalmFingerSegementPositionDialog = function( title, pfsp, callback, initValue, review = false )
    {
        var div = $( '<div />' );
        var id_base = 'pianos4_pfspdlg_' + ( new Date().getTime() );
        
        var width = 1243;
        var height = 656;
        var imgfactor = width / height;
        
        var img_dir = 'css/location';
        var basename = 'main';
        var baseimg = basename + '.png';
        
        var selected_areas = initValue || [];
        
        var mode = {
            a: 'individual',
            b: 'sync',
        }
        
        var getRelatedAreas = function( data, zone, lat )
        {
            data = data || [];
            
            if( data.length > 0 )
            {
                if( mode.a === 'zonal' )
                {
                    var globalZones = [];
                    data.forEach( function( x )
                    {
                        globalZones.push( x.split( '-' )[ 1 ] );
                    } );
                    globalZones = $.unique( globalZones );
                    
                    if( typeof zone === 'undefined' )
                        zone = globalZones;
                    
                    var sameZone = [];
                    
                    sameZone = Pianos4Constants.getItemsByVar(
                        Pianos4Constants.conclusions.analysis.palm_finger_segment_position,
                        'zone',
                        zone
                    );
                    
                    if( mode.b === 'splitted' )
                    {
                        sameZone = Pianos4Constants.getItemsByVar(
                            sameZone,
                            'lat',
                            lat
                        );
                    }
                    
                    var data = [];
                    sameZone.forEach( function( x )
                    {
                        data = [ ...data, ...x.sel ];
                    } );
                }
                data = $.unique( data );
            }
            
            return data;
        }
        
        var updateSelection = function( data )
        {
            data = data || [];
            
            selected_areas = $.unique( selected_areas );
            
            data = [ basename ]
                .concat( selected_areas )
                .concat( data )
                .map( x => 'url( ' + img_dir + '/' + x + '.png )' )
                .join( ', ' );
            
            $( '#location_img' )
                .css( 'background-image', data );
        }
        
        var getAndUpdateSelection = function( data, zone, lat )
        {
            data = getRelatedAreas( data, zone, lat );
            updateSelection( data );
        }
        
        var updateSelectionMode = function( e, type, value )
        {
            e.preventDefault();
            
            mode[ type ] = value;
            
            if( mode.a == 'individual' )
            {
                $( '#pfsp_mode_a_individual' )
                    .css( 'text-decoration', 'underline' );
                $( '#pfsp_mode_a_zonal' )
                    .css( 'text-decoration', 'none' );
                
                $( '#pfsp_mode_b_sync' )
                    .css( 'text-decoration', 'none' )
                    .css( 'color', '#cccccc' );
                $( '#pfsp_mode_b_splitted' )
                    .css( 'text-decoration', 'none' )
                    .css( 'color', '#cccccc' );
                $( '#pfsp_mode_b_separator' )
                    .css( 'text-decoration', 'none' )
                    .css( 'color', '#cccccc' );
            } else {
                $( '#pfsp_mode_a_individual' )
                    .css( 'text-decoration', 'none' );
                $( '#pfsp_mode_a_zonal' )
                    .css( 'text-decoration', 'underline' );
                
                if( mode.b == 'sync' )
                {
                    $( '#pfsp_mode_b_sync' )
                        .css( 'text-decoration', 'underline' );
                    $( '#pfsp_mode_b_splitted' )
                        .css( 'text-decoration', 'none' );
                } else {
                    $( '#pfsp_mode_b_sync' )
                        .css( 'text-decoration', 'none' );
                    $( '#pfsp_mode_b_splitted' )
                        .css( 'text-decoration', 'underline' );
                }
                $( '#pfsp_mode_b_sync' )
                    .css( 'color', '#000000' );
                
                $( '#pfsp_mode_b_splitted' )
                    .css( 'color', '#000000' );
                
                $( '#pfsp_mode_b_separator' )
                    .css( 'color', '#000000' );
            }
        }
        
        var updateMapCoordinates = function( data, fac )
        {
            data.children( 'area' ).each( function( i )
            { 
                $( this )[ 0 ].coords = $( this )[ 0 ].coords
                    .split( ',' )
                    .map( x => x * fac )
                    .join( ',' );
            } );
        }
        
        /*
         *  Modes of selection: individual / zonal and sync / splitted
         */
        var modes = $( '<div />' )
            .attr( 'id', 'pfsp_modeLinks' )
            .attr( 'width', '50%' )
            .css( 'float', 'right' );
        
        modes.append(
            $( '<a />' )
                .attr( 'id', 'pfsp_mode_a_individual' )
                .text( 'Individual' )
                .css( 'text-decoration', 'underline' )
                .click( function( e ){ updateSelectionMode( e, 'a', 'individual' ) } )
        )
        .append(
            $( '<span />' )
                .text( ' | ' )
        )
        .append(
            $( '<a />' )
                .attr( 'id', 'pfsp_mode_a_zonal' )
                .text( 'Zonal' )
                .click( function( e ){ updateSelectionMode( e, 'a', 'zonal' ) } )
        )
        .append(
            $( '<span />' )
                .text( ' / ' )
                .css( 'margin-right', '10px' )
                .css( 'margin-left', '10px' )
        )
        .append(
            $( '<a />' )
                .attr( 'id', 'pfsp_mode_b_sync' )
                .text( 'Snycronous' )
                .css( 'color', '#cccccc' )
                .click( function( e ){ updateSelectionMode( e, 'b', 'sync' ) } )
        )
        .append(
            $( '<span />' )
                .attr( 'id', 'pfsp_mode_b_separator' )
                .text( ' | ' )
                .css( 'color', '#cccccc' )
        )
        .append(
            $( '<a />' )
                .attr( 'id', 'pfsp_mode_b_splitted' )
                .text( 'Splitted' )
                .css( 'color', '#cccccc' )
                .click( function( e ){ updateSelectionMode( e, 'b', 'splitted' ) } )
        );
        
        /*
         *  Map data, with image preloading
         */
        var map = $( '<map />' )
            .attr( 'id', 'Map' )
            .attr( 'name', 'location_map' )
            .attr( 'class', 'location_map' )
            .css( 'text-decoration', 'none' );
        
        var preloader = [];
        
        for( var i in pfsp )
        {
            var sz = pfsp[ i ];
            
            preloader = [ ...preloader, ...sz.sel ];
            
            if( ! review )
            {
                map.append(
                    $( '<area /> ' )
                        .attr( 'shape', 'polygon' )
                        .attr( 'coords', sz.coords )
                        .attr( 'data-selection', sz.sel )
                        .attr( 'data-desc', sz.desc )
                        .attr( 'data-zone', sz.zone )
                        .attr( 'data-lat', sz.lat )
                        .mouseover( function()
                        {
                            $( '#pfsp_current_hover' )
                                .text( $( this )[ 0 ].dataset.desc );
                            
                            var s = $( this )[ 0 ].dataset.selection.split( ',' );
                            var zone = $( this )[ 0 ].dataset.zone;
                            var lat = $( this )[ 0 ].dataset.lat;
                            
                            getAndUpdateSelection( s, zone, lat );
                        } )
                        .mouseout( function()
                        {
                            $( '#pfsp_current_hover' )
                                .text( 'None' );
                            
                            updateSelection();
                        } )
                        .on( 'click', function( e )
                        {
                            e.defaultPrevented;
                            
                            var sel = $( this )[ 0 ].dataset.selection.split( ',' );
                            var zone = $( this )[ 0 ].dataset.zone;
                            var lat = $( this )[ 0 ].dataset.lat;
                            var desc = $( this )[ 0 ].dataset.desc;
                            
                            var clicked = getRelatedAreas( sel, zone, lat );
                            
                            var alreadySelected = function( a )
                            {
                                return selected_areas.indexOf( a ) > -1;
                            }
                            
                            if( desc == 'Left full palm'  || desc == 'Left upper palm'  || desc == 'Left lower palm' ||
                                desc == 'Right full palm' || desc == 'Right upper palm' || desc == 'Right lower palm' ||
                                desc == 'Left tips' || desc == 'Right tips' )
                            {
                                if( clicked.every( alreadySelected ) )
                                {
                                    for( var i in clicked )
                                        selected_areas.splice( selected_areas.indexOf( clicked[ i ] ), 1 );
                                
                                } else {
                                    selected_areas = [ ...selected_areas, ...clicked ];
                                }
                            } else {
                                var addToList = selected_areas.indexOf( sel[ 0 ] ) == -1;
                                
                                for( var i in clicked )
                                {
                                    var v = clicked[ i ];
                                    
                                    if( addToList )
                                        selected_areas.push( v );
                                    else
                                        selected_areas.splice( selected_areas.indexOf( v ), 1 );
                                }
                            }
                            
                            updateSelection();
                        } )
                );
            }
        }
        
        $.unique( preloader ).forEach( function( img ){
            $( '<img />' ).attr( 'src', img_dir + '/' + img + '.png' );
        } );
        
        /*
         *  Currently selected areas
         */
        var hover = $( '<div />' )
            .attr( 'width', '50%' )
            .css( 'float', 'left' )
            .append(
                $( '<span />' )
                    .text( 'Current hover: ' )
            )
            .append(
                $( '<span />' )
                    .attr( 'id', 'pfsp_current_hover' )
                    .css( 'font-style', ' italic' )
                    .text( 'None' )
            );
        
        /*
         *  Construction of the dialog
         */
        div.append(
            $( '<div />' )
                .text( 'Please select one or multiples potentials zones:' )
        );
        
        div.append(
            $( '<div />' )
                .attr( 'id', 'location_img' )
                .append(
                    $( '<img />' )
                        .width( '100%' )
                        .height( 'auto' )
                        .attr( 'src', img_dir + '/empty.png' )
                        .attr( 'usemap', '#location_map' )
                )
                .css( 'background-image', 'url( ' + img_dir + '/' + baseimg + ' )' )
                .css( 'background-repeat', 'no-repeat' )
                .css( 'background-size', '100%' )
        );
        
        div.append( map );
        
        if( ! review )
        {
            div.append(
                $( '<div />' )
                    .css( 'width', '100%' )
                    .attr( 'id', 'pfsp_legend' )
                    
                    .append( hover )
                    .append( modes )
            );
        }
        
        /*
         *  Dialog box resizing
         */
        
        var dlgwidth  = width;
        var dlgheight = height;
        
        if( ( $( window ).width() * 0.95 < width + 25 ) ) 
        {
            var fac = ( 0.95 * $( window ).width() - 25 ) / width;
            
            dlgwidth *= fac;
            dlgheight *= fac;
            
            updateMapCoordinates( map, fac );
        }
        
        if( $( window ).height() * 0.95 < height + 200 )
        {
            var fac = ( 0.95 * $( window ).height() - 200 ) / height;
            
            dlgwidth *= fac;
            dlgheight *= fac;
            
            updateMapCoordinates( map, fac );
        }
        
        dlgwidth += 25;
        dlgheight += 165;
        
        var buttons = null;
        
        if( ! review )
        {
            buttons = {
                'I dont know / None': function()
                {
                    $( this ).dialog( 'close' );
                    callback( [] );
                },
                'OK': function()
                {
                    $( this ).dialog( 'close' );
                    callback( $.unique( selected_areas ).sort() );
                },
                'Cancel': function()
                {
                    $( this ).dialog( 'close' );
                },
                'Clear': function()
                {
                    selected_areas = [];
                    updateSelection();
                }
            };
        } else {
            buttons = {
                'Close': function()
                {
                    $( this ).dialog( 'close' );
                },
            }
        }
        
        var dlgopts = {
            autoOpen: true,
            modal: true,
            title: title,
            close: function()
            {
                $( this ).remove();
            },
            width: dlgwidth,
            height: dlgheight,
            resizable: false,
            buttons: buttons,
            open: function( event, ui )
            {
                updateSelection( selected_areas )
            },
        };
        
        var dlg = $( '<div />' ).addClass( 'pianos4_dialog' ).append( div );
        
        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 )
    {
        assert( 'width' in obj && 'height' in obj && 'diag' in 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
                                {
                                    toastr.warning( 'Preferences could not be saved. They will last only for this session.', 'Warning', { timeout: 5000 } );
                                }
                            },
                            error: function()
                            {
                                toastr.warning( 'Preferences could not be saved. They will last only for this session.', 'Warning', { timeout: 5000 } );
                            },
                        } );
                    }
                },
                '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()
    {
        assert( params.getConclusionsScript );
        var complete = function( data )
        {
            var dlgopts = {
                autoOpen: true,
                modal: true,
                width: '80%',
                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 ),
                },
            } );
        }
        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' );
                            assert( udata );
                            log( udata );
                            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 );
                                }
                            }
                            log( udata );
                        } ) );
                    },
                }, {
                    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;
                        assert( mark_value === null || ( mark_value >= 0 && mark_value <= 3 ) );
                        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;
                        assert( decision === null || ( decision >= 0 && decision <= 2 ) );
                        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()
    {
        assert( params.resultsFilterScript );
        
        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
        assert( aconcl !== null && cconcl !== null );
        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;
    }
    
    /*
     *  Misc configuration
     */
    if( typeof( toastr ) !== "undefinded" )
    {
        toastr.options = {
            "closeButton": true,
            "debug": false,
            "newestOnTop": true,
            "progressBar": true,
            "positionClass": "toast-top-right",
            "preventDuplicates": false,
            "onclick": null,
            "showDuration": "300",
            "hideDuration": "300",
            "timeOut": "5000",
            "extendedTimeOut": "500",
            
            "showEasing": "easeOutBounce",
            "hideEasing": "easeInBack",
            
            "showMethod": "slideDown",
            "hideMethod": "slideUp",
            "closeMethod": "slideUp",
        }
    }
    
    markers = {
        factor: 1,
        lq2: null,
    };
    
    rotationView = {
        mark: {
            theta: 0,
            x: 0,
            y: 0,
        },
        print: {
            theta: 0,
            x: 0,
            y: 0,
        },
    };
    
}
