/*
 * PiAnoS (Picture Annotation System), A fingerprint examiner training tool
 * Copyright (C) 2011-2021 University of Lausanne, Switzerland
 * https://ips-labs.unil.ch/pianos
 * 
 * This program is free software: you can redistribute it and/or modify it under
 * the terms of the GNU Affero General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any
 * later version.
 * 
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
 * details.
 * 
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see http://www.gnu.org/licenses/.
 */

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

                assert(annotation.id in allAnnotations);
                selection[annotation.id] = true;
                
                var type = self.getAnnotationType(annotation.id);
                
                var node;
                if (params.useOtherFeaturesLayer && type == 'area' && 'type' in annotation && annotation.type == 'wcs')
                    node = d3svg.select('.pianos4_others #'+type+'_'+annotation.id); // This
                                                                                        // is
                                                                                        // hackish,
                                                                                        // but
                                                                                        // anyway
                                                                                        // the
                                                                                        // association
                                                                                        // between
                                                                                        // feature
                                                                                        // type
                                                                                        // and
                                                                                        // layers
                                                                                        // must
                                                                                        // be
                                                                                        // rethinked.
                else
                    node = d3svg.select('.pianos4_'+annotGroupByContent[type]+' #'+type+'_'+annotation.id);
                assert(!node.empty());
                
                if (type == 'minutia')
                {
                    assert(node.length == 1 && node.select('use._gizmo').empty());
                    var use = node.select('use').attr('href');
                    node.append('svg:use')
                        .classed('_rot', true)
                        .attr('xlink:href', use+'_gizmo')
                        .style('visibility','hidden')
                        .attr('transform','scale(2)')
                        .attr('pointer-events','all');
                    node.append('svg:use')
                        .classed('_gizmo', true)
                        .attr('xlink:href', use+'_gizmo')
                        .attr('stroke-width', 1.0)
                        .attr('fill-opacity', 0.3);
                }
                else if (type == 'ridge' || type == 'area')
                {
                    var svgridge = node.select('path');
                    var gizmoWidth = parseFloat(svgridge.attr('stroke-width'))+4;
                    node.insert('svg:path', 'path')
                        .classed('_gizmo', true)
                        .attr('stroke', svgridge.attr('stroke'))
                        .attr('stroke-width', gizmoWidth)
                        .attr('stroke-opacity', parseFloat(svgridge.attr('stroke-opacity') ? svgridge.attr('stroke-opacity')/2. : 0.5))
                        .attr('fill','none')
                        .attr('d', svgridge.attr('d'));
                    
                    for (var i in annotation.points)
                    {
                        var p = annotation.points[i];
                        node.append('svg:circle')
                            .classed('_handle', true)
                            .attr('id', 'ridge_'+annotation.id+'_'+i)
                            .attr('stroke', 'black')
                            .attr('stroke-width', 1)
                            .attr('fill', 'white')
                            .attr('cx', p[0])
                            .attr('cy', p[1])
                            .attr('r', gizmoWidth/2.0+1.0);
                    }
                }
            }
        }
        
        if (typeof(noEvent) == 'boolean' && noEvent)
            return;
        
        pianos.onSelectionChanged(self);
    }
    
    this.unselectAnnotation = function(aid, noEvent, noRemove)
    {
        var type = self.getAnnotationType(aid);
        var annotation = allAnnotations[aid][1];
        var node;
        if (params.useOtherFeaturesLayer && type == 'area' && 'type' in annotation && annotation.type == 'wcs')
            node = d3svg.select('.pianos4_others #'+type+'_'+annotation.id); // This
                                                                                // is
                                                                                // hackish,
                                                                                // but
                                                                                // anyway
                                                                                // the
                                                                                // association
                                                                                // between
                                                                                // feature
                                                                                // type
                                                                                // and
                                                                                // layers
                                                                                // must
                                                                                // be
                                                                                // rethinked.
        else
            node = d3svg.select('.pianos4_'+annotGroupByContent[type]+' #'+type+'_'+annotation.id);
        assert(!node.empty());
        assert(node.length == 1);
        assert(node.select('._gizmo').length == 1);
        node.select('._gizmo').remove();
        
        if (type == 'minutia') {
            assert(node.select('use._rot').length == 1);
            node.select('use._rot').remove();
        }
        else if (type == 'ridge' || type == 'area') {
            node.selectAll('._handle').remove();
        }
        
        if (typeof(noRemove) == 'undefined' || !noRemove)
            delete selection[aid];
        
        if (typeof(noEvent) == 'boolean' && noEvent)
            return;
        
        pianos.onSelectionChanged(self);
    }
    
    this.getSelection = function()
    {
        return selection;
    }
    
    this.getSelectionSize = function()
    {
        var count = 0;
        for (var i in selection)
            ++count;
        return count;
    }
    
    this.deleteSelection = function()
    {
        var action = null;
        for (aid in selection)
        {
            if (!action)
                action = new Pianos4Action_Batch();
            var rem = new Pianos4Action_DeleteAnnotation(self, aid);
            action.append(rem);
        }
        if (action)
        {
            // self.selectAnnotation(null, true);
            pianos.exec(action);
        }
    }
    
    this.isSelected = function(annotation)
    {
        return annotation.id in selection;
    }
    
    this.setMinutiaMarkerSize = function(factor)
    {    
        markers.factor = factor;
        var items = ['minutia_bifurcation', 'minutia_ridge_ending', 'minutia_unknown', 'minutia_unknown2', 'minutia_center', 'minutia_difference'];
        
        var defs = d3svg.select('defs');
        
        var suffixes = ['', '_alt'];
        for (var s in suffixes)
        {
            var suffix = suffixes[s];
            for (var i in items)
            {
                var marker = defs.select('#'+items[i]+suffix);
                assert(marker.length == 1);
                var gizmo = defs.select('#'+items[i]+suffix+'_gizmo');
                assert(gizmo.length == 1);
                
                if (factor == 1.0)
                {
                    // Just to keep things clean, remove the transform attribute
                    // when the factor is 1 - it's a no-op anyway.
                    marker.node().removeAttribute('transform');
                    gizmo.node().removeAttribute('transform');
                }
                else
                {
                    marker.attr('transform', 'scale('+factor+')');
                    gizmo.attr('transform', 'scale('+factor+')');
                }
            }
        }
    }
    
    this.setMinutiaMarkerTransparency = function(factor)
    {    
        var items = ['minutia_bifurcation', 'minutia_ridge_ending', 'minutia_unknown', 'minutia_unknown2', 'minutia_center', 'minutia_difference'];
        
        var defs = d3svg.select('defs');
        
        var suffixes = ['', '_alt'];
        for (var s in suffixes)
        {
            var suffix = suffixes[s];
            for (var i in items)
            {
                var marker = defs.select('#'+items[i]+suffix);
                assert(marker.length == 1);
                var gizmo = defs.select('#'+items[i]+suffix+'_gizmo');
                assert(gizmo.length == 1);

                marker.attr('opacity', factor);
                gizmo.attr('opacity', factor);
            }
        }
    }
    
    this.setPairingMarkerSize = function(factor)
    {    
        d3svg.selectAll( "text.pianos4_pair" ).attr( 'font-size', factor + 'em' )
    }
    
    this.setSegments = function(segs)
    {
        layers.segments.selectAll('g').remove();
        segments = segs;
        if (segments)
            drawSegments();
    }

    this.setUserResultsVisible = function(userID, visible)
    {
        assert(params.splitByUser);
        $(['minutiae','ridges','areas','comments','others','pairing']).each(function() {
            if (!(this in layers))
                return;
            var layer = layers[this];
            var g = layer.select('g#'+this+'_user_'+userID);
            if (!g.empty())
            {
                if (visible)
                    g.style('display', null);
                else
                    g.style('display', 'none');
            }
        });
        // var g = layers
    }
    
    this.addH5CanvasCommand = function(command)
    {
        assert(params.useH5Canvas);
        
        h5CanvasCommands.push(command);
        drawH5CanvasCommand(command);
    }
    
    // Sets whether new annotations should be added to the SVG but kept hidden
    // by setting the user's parent node to 'display=none'. This can only be
    // used when params.splitByUser is true - otherwise we would have to either
    // hide every single annotation OR hide complete layers.
    // This does NOT show/hide existing annotations and will only work for new
    // per-user svg:g nodes. Use it before and after calling a batch of
    // this.setData().
    // See pianos.setData().
    this.setNewAnnotationsHidden = function(hidden)
    {
        assert(params.splitByUser);
        hideNewAnnotations = hidden;
    }
    
    // ------------------------------
    // Private variables
    // ------------------------------
    
    var self = this;
    var d3svg = null;
    var layers = null;
    var zoom = 1.0;
    var annotations = {
        minutiae: {},
        ridges: {},
        areas: {},
        comments: {},
        GC: {},
    };
    var allAnnotations = {};
    var annotGroupByContent = {};
    var capturingTool = null;
    var origin = [0,0];
    var originLocked = false;
    var temporaryTool = null;
    var selection = {};
    var filters = null;
    var segments = null;
    var h5Canvas = null;
    var h5CanvasCommands = [];
    var hideNewAnnotations = false;
    
    
    // ------------------------------
    // Private functions
    // ------------------------------
    
    this.updateRotation = function()
    {
        var rotData = rotationView[ params.type ];
        
        $( ".pianos4_" + params.type + " .pianos4_root" )
            .attr( "transform","rotate( " + rotData.theta + " " + rotData.x + " " + rotData.y + " )" );
    }
    
    var buildUI = function()
    {
        params.div
            .addClass('pianos4_view')
            .data('Pianos4View', self);
        
        var canvas = $('<div class="pianos4_view_canvas"></div>');
        
        var rotationSlider = $( '<div />' )
            .attr( 'id', 'rotationslider_' + params.type )
            .slider( {
                min: -180,
                max: 180,
                value: 0,
                orientation: 'horizontal',
                slide: function( event, ui )
                {
                    var cx = params.image.width / 2;
                    var cy = params.image.height / 2;
                    rotationView[ params.type ] = {
                        theta: ui.value,
                        x: cx,
                        y: cy,
                    };
                    
                    self.updateRotation();
                },
            } );
        
        // Set the default rotation and center of rotation
        rotationView[ params.type ] = {
            theta: 0,
            x: params.image.width / 2,
            y: params.image.height / 2,
        };
        
        params.div.append( rotationSlider );
        
        params.div.append(canvas);
        
        d3svg = d3.select(params.path + ' .pianos4_view_canvas')
            .append("svg:svg")
            // .attr("class", "chart")
            .attr("width", canvas.width())
            .attr("height", canvas.height());
            
        // SVG Definitions
        var defs = d3svg.append('svg:defs');
        var mgizmo_color = '#00aa00';

        var minutiaData = [
            {suffix: '', color: '#ff0000'},
            {suffix: '_alt', color: '#ffff00'},
        ];
        
        /*
         *    ↑ ╔═════════╗
         *    | ║         ║
         *  h | ║    .    ╠═══════════
         *    | ║         ║
         *    ↓ ╚═════════╝
         *            ←―――→←―――――――――→
         *              r      lq
         *              
         *            ←――――――――――――――→
         *                   lq2
         *                   
         *   The size of the minutiae have to be specified as shown above. The 
         *   values (h = 0.5mm and lq = 0.8mm) used by default are compatible
         *   with all Morpho AFIS (especially MetaMorpho and MorphoBis). 
         */
        
        var h = 0.5;
        var lq2 = 1.05;
        var s = 1.5;
        
        // Change the default size in regards of the resolution of the image
        h *= params.image.resolution / 500
        lq2 *= params.image.resolution / 500
        s *= params.image.resolution / 500
        
        // Convert the size to px @ 500dpi
        var h = h * 500 / 25.4;
        var lq2 = lq2 * 500 / 25.4;
        markers.lq2 = lq2;
        
        var r = h / 2;
        var lq = lq2 - r;
        var s2 = 2 * s;
        var sin60 = Math.sin( Math.PI / 3 );
        var cos60 = Math.cos( Math.PI / 3 );
        
        for (var minDataIndex in minutiaData)
        {
            var minData = minutiaData[minDataIndex];
            
            // Bifurcation minutia marker
            var bif = defs.append('svg:g')
                .attr('id', 'minutia_bifurcation'+minData.suffix);
            // White outline
            bif.append('svg:path')
                .attr('d', 'M ' + r + ' 0 L ' + r + ' ' + -r + ' L ' + -r + ' ' + -r + ' L ' + -r + ' ' + r + ' L ' + r + ' ' + r + ' L ' + r + ' 0 L ' + ( r + lq ) + ' 0')
                .attr('stroke-width', s2)
                .attr('stroke', '#ffffff')
                .attr('fill', 'none');
            // Colored marker
            bif.append('svg:path')
                .attr('d', 'M ' + r + ' 0 L ' + r + ' ' + -r + ' L ' + -r + ' ' + -r + ' L ' + -r + ' ' + r + ' L ' + r + ' ' + r + ' L ' + r + ' 0 L ' + ( r + lq ) + ' 0')
                .attr('stroke-width', s)
                .attr('stroke', minData.color)
                .attr('fill', 'none');
            // Selectable area (not visible)
            var bif_gizmo = defs.append('svg:rect')
                .attr('id', 'minutia_bifurcation'+minData.suffix+'_gizmo')
                .attr('x', -r - s)
                .attr('y', -r - s)
                .attr('width', s + 2 * r + lq)
                .attr('height', h + s2)
                .attr('stroke', mgizmo_color)
                .attr('fill', mgizmo_color);
            
            // Ridge ending minutia marker
            var re = defs.append('svg:g')
                .attr('id', 'minutia_ridge_ending'+minData.suffix);
            // White outline
            re.append('svg:circle')
                .attr('cx', 0)
                .attr('cy', 0)
                .attr('r', r)
                .attr('stroke-width', s2)
                .attr('stroke', '#ffffff')
                .attr('fill', 'none');
            re.append('svg:path')
                .attr('d', 'M ' + r + ' 0 L ' + ( r + lq ) + ' 0')
                .attr('stroke-width', s2)
                .attr('stroke', '#ffffff')
                .attr('fill', 'none');
            // Colored marker
            re.append('svg:circle')
                .attr('cx', 0)
                .attr('cy', 0)
                .attr('r', r )
                .attr('stroke-width', s)
                .attr('stroke', minData.color)
                .attr('fill', 'none');
            re.append('svg:path')
                .attr('d', 'M ' + r + ' 0 L ' + ( r + lq ) + ' 0')
                .attr('stroke-width', s)
                .attr('stroke', minData.color)
                .attr('fill', 'none');
            // Selectable area (not visible)
            var re_gizmo = defs.append('svg:rect')
                .attr('id', 'minutia_ridge_ending'+minData.suffix+'_gizmo')
                .attr('x', -r - s)
                .attr('y', -r - s)
                .attr('width', s + 2 * r + lq )
                .attr('height', h + s2 )
                .attr('stroke', mgizmo_color)
                // .attr('stroke-width', 0.0)
                .attr('fill', mgizmo_color);
                // .attr('fill-opacity', 0.0);
            
            // Unknown minutia marker
            var unk = defs.append('svg:g')
                .attr('id', 'minutia_unknown'+minData.suffix);
            // White outline
            unk.append('svg:path')
                .attr('d', 'M ' + r + ' 0 L ' + -r * cos60 + ' ' + -r * sin60 + ' L ' + -r * cos60 + ' ' + r * sin60 + ' L ' + r + ' 0 L ' + ( r + lq ) + ' 0')
                .attr('stroke-width', s2)
                .attr('stroke', '#ffffff')
                .attr('fill', 'none');
            // Colored marker
            unk.append('svg:path')
                .attr('d', 'M ' + r + ' 0 L ' + -r * cos60 + ' ' + -r * sin60 + ' L ' + -r * cos60 + ' ' + r * sin60 + ' L ' + r + ' 0 L ' + ( r + lq ) + ' 0')
                .attr('stroke-width', s)
                .attr('stroke', minData.color)
                .attr('fill', 'none');
            // Selectable area (not visible)
            var unk_gizmo = defs.append('svg:rect')
                .attr('id', 'minutia_unknown'+minData.suffix+'_gizmo')
                .attr('x', -r - s)
                .attr('y', -r - s)
                .attr('width', s + 2 * r + lq)
                .attr('height', h + s2)
                .attr('stroke', mgizmo_color)
                // .attr('stroke-width', 0.0)
                .attr('fill', mgizmo_color);
                // .attr('fill-opacity', 0.0);
            
            // Unknown2 minutia marker
            var unk2 = defs.append('svg:g')
                .attr('id', 'minutia_unknown2'+minData.suffix);
            // White outline
            unk2.append('svg:path')
                .attr('d', 'M ' + r + ' 0 L 0 ' + -0.8 * r + ' L ' + -r + ' 0 L 0 ' + 0.8 * r + ' L ' + r + ' 0 L ' + ( r + lq ) + ' 0')
                .attr('stroke-width', s2)
                .attr('stroke', '#ffffff')
                .attr('fill', 'none');
            // Colored marker
            unk2.append('svg:path')
                .attr('d', 'M ' + r + ' 0 L 0 ' + -0.8 * r + ' L ' + -r + ' 0 L 0 ' + 0.8 * r + ' L ' + r + ' 0 L ' + ( r + lq ) + ' 0')
                .attr('stroke-width', s)
                .attr('stroke', minData.color)
                .attr('fill', 'none');
            // Selectable area (not visible)
            var unk2_gizmo = defs.append('svg:rect')
                .attr('id', 'minutia_unknown2'+minData.suffix+'_gizmo')
                .attr('x', -r - s)
                .attr('y', -r - s)
                .attr('width', s + 2 * r + lq)
                .attr('height', h + s2)
                .attr('stroke', mgizmo_color)
                // .attr('stroke-width', 0.0)
                .attr('fill', mgizmo_color);
                // .attr('fill-opacity', 0.0);

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

        // GhostCursor Anchor marker
        var GCAllData = [
            { suffix: '_user',      color: '#0000ff' },
            { suffix: '_projected', color: '#600060' },
        ];
        
        for ( var GCDataIndex in GCAllData )
        {
            var GCData = GCAllData[ GCDataIndex ];
            
            var GC = defs.append('svg:g')
                .attr('id', 'GC'+GCData.suffix);
            
            GC.append('svg:path')
                .attr('d', 'm0.0068, -17.00255c-3.51966, 0 -6.37336, 2.8537 -6.37336, 6.37336c0, 1.43361 0.47302, 2.75515 1.27178, 3.8205l5.10157, 6.80176l5.10161, -6.80176c0.79873, -1.06535 1.27175, -2.38689 1.27175, -3.8205c0, -3.51966 -2.8537, -6.37336 -6.37336, -6.37336l0.00001, 0zm0, 10.62226c-2.34646, 0 -4.2489, -1.90248 -4.2489, -4.2489s1.90248, -4.2489 4.2489, -4.2489s4.2489, 1.90248 4.2489, 4.2489s-1.90244, 4.2489 -4.2489, 4.2489z')
                .attr('stroke-width', s*0.4)
                .attr('stroke', '#ffffff') 
                .attr('fill', GCData.color)
                .attr('transform','scale( ' + 0.8 * s + ' )');
        }
        
        var GC_live = defs.append('svg:g')
            .attr('id', 'GC_live')
            .attr('opacity',0.8);
        
        var GC_live_data = [
            { size: s2, color: '#ffffff' },
            { size: s,  color: '#ff0000' },
        ]
        
        for( var i = 0; i < 4; i++ )
        {
            for( var GC_live_data_index = 0; GC_live_data_index < GC_live_data.length; GC_live_data_index++ )
            {
                var d = GC_live_data[ GC_live_data_index ];
                
                GC_live.append('svg:line')
                    .attr('x1', 5 )
                    .attr('x2', 20 )
                    .attr('y1', 0 )
                    .attr('y2', 0 )
                    .attr('stroke-width', d.size)
                    .attr('stroke', d.color)
                    .attr('fill', d.color)
                    .attr('transform','rotate('+90*i+')');
            }
        }
        
        // Actual content of the SVG

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

        // Layers
        layers = {
            images: root.append('svg:g').attr('class', 'pianos4_layer pianos4_images'),
            helpers: root.append('svg:g').attr('class', 'pianos4_layer pianos4_helpers'),
            areas: root.append('svg:g').attr('class', 'pianos4_layer pianos4_areas'),
            others: root.append('svg:g').attr('class', 'pianos4_layer pianos4_others'),
            ridges: root.append('svg:g').attr('class', 'pianos4_layer pianos4_ridges'),
            minutiae: root.append('svg:g').attr('class', 'pianos4_layer pianos4_minutiae'),
            GC: root.append('svg:g').attr('class', 'pianos4_layer pianos4_GC'),
            TPSlines: root.append('svg:g').attr('class', 'pianos4_layer pianos4_TPSlines'),
            segments: root.append('svg:g').attr('class', 'pianos4_layer pianos4_segments'),
            pairing: root.append('svg:g').attr('class', 'pianos4_layer pianos4_pairing'),
            ruler: root.append('svg:g').attr('class', 'pianos4_layer pianos4_ruler'),
            comments: d3svg.append('svg:g').attr('class', 'pianos4_layer pianos4_comments'),
        };
        self.toggleLayer('pairing', false);
        self.toggleLayer('segments', false);
        
        // Finger-print/mark Image
        var image = layers.images.append('svg:image')
            .attr('x', 0)
            .attr('y', 0)
            .attr('width', params.image.width+'px')
            .attr('height', params.image.height+'px')
            .attr('xlink:href', params.image.href);
        
        // Ruler
        var pxpermm = params.image.resolution / 25.4;
        
        var ruler = layers.ruler;
        ruler.classed( 'pianos4_helpers', true );
        ruler.attr( 'id', 'pianos4_ruler_' + params.type );
        
        var ruler_top = ruler.append( 'svg:g' );
        var ruler_left = ruler.append( 'svg:g' );
        var ruler_info = ruler.append( 'svg:g' );
        
        ruler_top.append( 'svg:rect' )
            .attr( 'y', -pxpermm * 1.2 )
            .attr( 'x', 0 )
            .attr( 'width', params.image.width )
            .attr( 'height', pxpermm * 1.2 )
            .attr( 'fill', '#fff' );
        
        for( var i = 0; i < params.image.width / params.image.resolution * 25.4; i++ )
        {
            var s = pxpermm * 0.5;
            if( i % 10 == 0 )
                s = pxpermm * 1;
            else if( i % 5 == 0 )
                s = pxpermm * 0.75;
            
            if( pxpermm * ( i + 0.1 ) < params.image.width )
            {
                ruler_top.append( 'svg:rect' )
                    .attr( 'y', -s )
                    .attr( 'x', i * pxpermm )
                    .attr( 'height', s )
                    .attr( 'width', pxpermm * 0.1 )
                    .attr( 'fill', '#000000' );
            }
        }
        
        ruler_left.append( 'svg:rect' )
            .attr( 'x', -pxpermm * 2 )
            .attr( 'y', 0 )
            .attr( 'height', params.image.height )
            .attr( 'width', pxpermm * 2 )
            .attr( 'fill', '#fff' );
        
        ruler_left.append( 'svg:rect' )
            .attr( 'x', -pxpermm * 2 )
            .attr( 'y', -pxpermm * 1.2 )
            .attr( 'height', pxpermm * 1.2 )
            .attr( 'width', pxpermm * 2 )
            .attr( 'fill', '#fff' );
        
        for( var i = 0; i < params.image.height / params.image.resolution * 25.4; i++ )
        {
            var s = pxpermm * 0.5;
            if( i % 10 == 0 )
                s = pxpermm * 1;
            else if( i % 5 == 0 )
                s = pxpermm * 0.75;
            
            if( pxpermm * ( i + 0.1 ) < params.image.height )
            {
                ruler_left.append( 'svg:rect' )
                .attr( 'x', -s )
                .attr( 'y', i * pxpermm )
                .attr( 'width', s )
                .attr( 'height', pxpermm * 0.1 )
                .attr( 'fill', '#000000' );
            }
        }
        ruler_left.append( 'svg:text' )
            .attr( 'id', 'ruler_text_' + params.type ) 
            .attr( 'x', 0 )
            .attr( 'y', pxpermm * 1.8 )
            .attr( 'font-size', pxpermm * 0.8 )
            .attr( 'transform', 'rotate( 90 )' )
            .text( "Synthetic scale based on the input resolution" );
        
        ruler_top.append( 'svg:text' )
            .attr( 'id', 'ruler_text_' + params.type )
            .attr( 'x', pxpermm * 0.2 )
            .attr( 'y', -pxpermm * 0.6 )
            .attr( 'font-size', pxpermm * 0.4 )
            .text( "[mm]" );
        
        ruler_info.append( 'svg:text' )
            .attr( 'id', 'pianos4_rulerinfo_' + params.type )
            .attr( 'font-size', pxpermm * 0.5 )
            .attr( 'stroke' , '#ffffff' )
            .attr( 'stroke-width', pxpermm * 0.1 )
            .attr( 'paint-order', 'stroke fill' )
            .attr( 'stroke-linejoin', 'round' )
            .attr( 'font-weight', 'bold' )
            .attr( 'fill', '#000000' );
        
        ruler_info.append( 'svg:rect' )
            .attr( 'id', 'pianos4_rulerline_' + params.type )
            .attr( 'x', 0 )
            .attr( 'y', -pxpermm * 0.05 )
            .attr( 'width', 0 )
            .attr( 'height', pxpermm * 0.1 )
            .attr( 'rx', pxpermm * 0.05 )
            .attr( 'ry', pxpermm * 0.05 )
            .attr( 'fill', '#ff0000' );
        
        // Event handlers
        d3svg.on('click', function(data,index) { 
                // All SVG elements with xlink:hrefs are not browseable targets
                // - so we should
                // not "open in a new window" on ctrl+click or middle-click. The
                // tools use the
                // 'mouse[down/move/up]' events anyway so we can disable the
                // 'click' event overall.
                // Note that this does the trick on IE, but not in Firefox,
                // where there seems to
                // be no way of preventing the middle-click from opening a
                // window - ctrl+click is
                // disabled, though.
                d3.event.preventDefault();
            }).on('mouseup', function() { d3.event.preventDefault(); });
            
        d3svg.on('mousedown', function(data,index) { 
            // Delegate all mouse events to the tool management subsystem
            d3.event.preventDefault(); 
            onMouseDown(d3.event); 
        });
        canvas.bind('mousewheel', onMouseWheel);
        pianos.registerHandler(['keydown', 'space'], onKeyDown).registerHandler(['keyup', 'space'], onKeyUp);
        pianos.registerHandler(['keydown', params.editToolHotkey], onKeyDown).registerHandler(['keyup', params.editToolHotkey], onKeyUp);
        pianos.registerHandler(['keydown', 'del'], onKeyDown).registerHandler(['keyup', 'del'], onKeyUp);
        pianos.registerHandler(['keydown', 'backspace'], onKeyDown).registerHandler(['keyup', 'backspace'], onKeyUp);
        pianos.registerHandler(['keydown', 'ctrl'], onKeyDown).registerHandler(['keyup', 'ctrl'], onKeyUp);

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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