view webapp/src/main/webapp/jquery/jquery.digilib.vector.js @ 1258:fd90ed468eec

improved vector update.
author robcast
date Fri, 17 Jan 2014 22:31:27 +0100
parents 4f8f960a4bea
children e70b62304290
line wrap: on
line source

/*
 * #%L
 * digilib vector plugin
 * %%
 * Copyright (C) 2014 MPIWG Berlin, Bibliotheca Hertziana
 * %%
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser 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 General Lesser Public License for more details.
 * 
 * You should have received a copy of the GNU General Lesser Public 
 * License along with this program.  If not, see
 * <http://www.gnu.org/licenses/lgpl-3.0.html>.
 * #L%
 * Authors: Robert Casties, Martin Raspe
 */

/**
 * digilib vector plugin.
 * 
 * Displays vector shapes on top of the image.
 * 
 * Shapes are objects with "geometry" and "properties" members.
 * Geometry is an object with "type" and "coordinates" members.
 * Types: Line, Rectangle. Coordinates are lists of pairs of relative coordinates.
 * Properties are SVG properties "stroke", "stroke-width", "fill" and other properties.
 * If a shape has an "id" member its value will be used in SVG.
 * 
 * shape = {
 *   'geometry' : {
 *     'type' : 'Line',
 *     'coordinates' : [[0.1, 0.2], [0.3, 0.4]]
 *   },
 *   'properties' : {
 *     'stroke' : 'blue'
 *   }
 * }
 * 
 */
(function($) {

    // affine geometry
    var geom = null;
    // plugin object with digilib data
    var digilib = null;
    // SVG namespace
    var svgNS = 'http://www.w3.org/2000/svg';
    
    
    var buttons = {
    };

    var defaults = {
        // is vector active?
        'isVectorActive' : true,
        // default SVG stroke
        'defaultStroke' : 'red',
        // default SVG stroke-width
        'defaultStrokeWidth' : '2',
        // default SVG fill
        'defaultFill' : 'none',
        // grab handle size
        'editHandleSize' : 10
    };

    var actions = {
        /**
         * set list of vector objects (shapes).
         * 
         * replaces existing shapes.
         * 
         * @param data
         * @param shapes
         */
        setShapes : function(data, shapes) {
        	data.shapes = shapes;
        	renderShapes(data);
        },
	
        /**
         * add vector object (shape) or create one by clicking.
         * 
         * For interactive use shape has to be initialized with a shape object with
         * type but no coordinates, e.g {'geometry':{'type':'Line'}}. The onComplete
         * function will be called with data and the new shape object as parameters.
         * 
         * @param data
         * @param shape
         * @param onComplete
         */
        addShape : function(data, shape, onComplete) {
        	if (data.shapes == null) {
        		data.shapes = [];
        	};
        	if (shape.geometry.coordinates == null) {
        		// define shape interactively
        		defineShape(data, shape, onComplete);
        	} else {
        		data.shapes.push(shape);
            	renderShapes(data);
        	}
        },
        
        /**
         * get vector object (shape) by id.
         * 
         * @param data
         * @param id
         * @returns shape
         */
        getShapeById : function(data, id) {
        	shapes = data.shapes;
        	if (shapes == null) return null;
        	for (var i in shapes) {
        		if (shapes[i].id === id) {
        			return shapes[i];
        		}
        	}
        	return null;
        },
        
        /**
         * remove vector object (shape) by id.
         * 
         * @param data
         * @param id
         */
        removeShapeById : function(data, id) {
        	shapes = data.shapes;
        	if (shapes == null) return;
        	for (var i in shapes) {
        		if (shapes[i].id === id) {
        			shapes.splice(i, 1);
        		}
        	}
        	displayShapes(data);
        }        	
    };

    // plugin installation routine, called by digilib on each plugin object.
    var install = function(plugin) {
        digilib = plugin;
        console.debug('installing vector plugin. digilib:', digilib);
        // import geometry classes
        geom = digilib.fn.geometry;
        // add defaults, actions, buttons to the main digilib object
        $.extend(digilib.defaults, defaults);
        $.extend(digilib.actions, actions);
        $.extend(digilib.buttons, buttons);
    };

    // plugin initialization
    var init = function (data) {
        console.debug('initialising vector plugin. data:', data);
        var $data = $(data);
        // install event handlers
        $data.bind('setup', handleSetup);
        $data.bind('update', handleUpdate);
    };


    var handleSetup = function (evt) {
        console.debug("vector: handleSetup");
        var data = this;
        //renderShapes(data);
    };

    /**
     * render list of shapes on screen.
     */
    var renderShapes = function (data) {
    	console.debug("renderShapes shapes:", data.shapes);
    	if (data.shapes == null || data.imgTrafo == null || !data.settings.isVectorActive) 
    	    return;
        if (data.$svg != null) {
        	data.$svg.remove();
        }
        var settings = data.settings;
    	var $svg = $(createSvg('svg', {
    	    'viewBox': data.imgRect.getAsSvg(),
    	    'class': settings.cssPrefix+'overlay',
    		'style': 'position:absolute; z-index:10; pointer-events:none;'}));
        // adjust svg element size and position (doesn't work with .adjustDiv())
        $svg.css(data.imgRect.getAsCss());
        data.$svg = $svg;
    	for (var i in data.shapes) {
    		var shape = data.shapes[i];
    		renderShape(data, shape, $svg);
    	}
    	data.$elem.append($svg);
    };
    
    /**
     * render a shape on screen.
     * 
     * Creates a SVG element and adds it to $svg.
     * Puts a reference to the element in the shape object.
     */
    var renderShape = function (data, shape, $svg) {
        if ($svg == null) {
            if (data.$svg == null) {
                renderShapes(data);
            }
            $svg = data.$svg;
        }
        var settings = data.settings;
        var css = settings.cssPrefix;
        var hs = settings.editHandleSize;
        var trafo = data.imgTrafo;
        // use given id
        var id = digilib.fn.createId(shape.id, css+'svg-');
        // set properties
        var props = shape.properties || {};
        var stroke = props['stroke'] || settings.defaultStroke;
        var strokeWidth = props['stroke-width'] || settings.defaultStrokeWidth;
        var fill = props['fill'] || settings.defaultFill;
        var coords = shape.geometry.coordinates;
        var gt = shape.geometry.type;
        if (gt === 'Line') {
            /*
             * Line
             */
            var p1 = trafo.transform(geom.position(coords[0]));
            var p2 = trafo.transform(geom.position(coords[1]));
            var $elem = $(createSvg('line', {
                'id': id,
                'x1': p1.x, 'y1': p1.y,
                'x2': p2.x, 'y2': p2.y,
                'stroke': stroke, 'stroke-width': strokeWidth}));
            shape.$elem = $elem;
            $svg.append($elem);
            if (props.editable) {
                var $e1 = $(createSvg('rect', {
                    'x': p1.x-hs/2, 'y': p1.y-hs/2, 'width': hs, 'height': hs,
                    'stroke': 'darkgrey', 'stroke-width': 1, 'fill': 'none',
                    'class': css+'svg-handle', 'style': 'pointer-events:all'}));
                var $e2 = $(createSvg('rect', {
                    'x': p2.x-hs/2, 'y': p2.y-hs/2, 'width': hs, 'height': hs,
                    'stroke': 'darkgrey', 'stroke-width': 1, 'fill': 'none',
                    'class': css+'svg-handle', 'style': 'pointer-events:all'}));
                var $vertexElems = [$e1, $e2];
                shape.$vertexElems = $vertexElems;
                $svg.append($vertexElems);
                $e1.one("mousedown.dlVertexDrag", getVertexDragHandler(data, shape, 0));
                $e2.one("mousedown.dlVertexDrag", getVertexDragHandler(data, shape, 1));
            }
        } else if (gt === 'Rectangle') {
            /*
             * Rectangle
             */
            var p1 = trafo.transform(geom.position(coords[0]));
            var p2 = trafo.transform(geom.position(coords[1]));
            var rect = geom.rectangle(p1, p2);
            var $elem = $(createSvg('rect', {
                'id': id,
                'x': rect.x, 'y': rect.y,
                'width': rect.width, 'height': rect.height,
                'stroke': stroke, 'stroke-width': strokeWidth,
                'fill': fill}));
            shape.$elem = $elem;
            $svg.append($elem);
            if (props.editable) {
                var $e1 = $(createSvg('rect', {
                    'x': p1.x-hs/2, 'y': p1.y-hs/2, 'width': hs, 'height': hs,
                    'stroke': 'darkgrey', 'stroke-width': 1, 'fill': 'none',
                    'class': css+'svg-handle', 'style': 'pointer-events:all'}));
                var $e2 = $(createSvg('rect', {
                    'x': p2.x-hs/2, 'y': p2.y-hs/2, 'width': hs, 'height': hs,
                    'stroke': 'darkgrey', 'stroke-width': 1, 'fill': 'none',
                    'class': css+'svg-handle', 'style': 'pointer-events:all'}));
                var $vertexElems = [$e1, $e2];
                shape.$vertexElems = $vertexElems;
                $svg.append($vertexElems);
                $e1.one("mousedown.dlVertexDrag", getVertexDragHandler(data, shape, 0));
                $e2.one("mousedown.dlVertexDrag", getVertexDragHandler(data, shape, 1));
            }
        }
    };

    var getVertexDragHandler = function (data, shape, vtx, onComplete) {
        var $document = $(document);
        var hs = data.settings.editHandleSize;
        var $shape = shape.$elem;
        var $handle = (shape.$vertexElems != null) ? shape.$vertexElems[vtx] : $();
        var shapeType = shape.geometry.type;
        var pt, pt0, pt1, pt2, rect;

        var dragStart = function (evt) {
            // cancel if not left-click
            if (evt.which != 1) return;
            pt0 = geom.position(evt);
            if (shapeType === 'Rectangle') {
                // save rectangle screen endpoints
                pt1 = data.imgTrafo.transform(geom.position(shape.geometry.coordinates[0]));
                pt2 = data.imgTrafo.transform(geom.position(shape.geometry.coordinates[1]));
            }
            $document.on("mousemove.dlVertexDrag", dragMove);
            $document.on("mouseup.dlVertexDrag", dragEnd);            
            return false;
        };
        
        var dragMove = function (evt) {
            pt = geom.position(evt);
            pt.clipTo(data.imgRect);
            // move handle
            $handle.attr({'x': pt.x-hs/2, 'y': pt.y-hs/2});
            // update shape element
            if (shapeType === 'Line') {
                if (vtx === 0) {
                    $shape.attr({'x1': pt.x, 'y1': pt.y});
                } else if (vtx === 1) {
                    $shape.attr({'x2': pt.x, 'y2': pt.y});
                }
            } else if (shapeType === 'Rectangle') {
                if (vtx === 0) {
                    rect = geom.rectangle(pt, pt2);
                } else if (vtx === 1) {
                    rect = geom.rectangle(pt1, pt);
                }
                $shape.attr({'x': rect.x, 'y': rect.y,
                    'width': rect.width, 'height': rect.height});               
            }
            return false;
        };

        var dragEnd = function (evt) {
            pt = geom.position(evt);
            if (pt.distance(pt0) < 5) {
                return false;
            }
            pt.clipTo(data.imgRect);
            var p1 = data.imgTrafo.invtransform(pt);
            // update shape element
            if (shapeType === 'Line') {
                shape.geometry.coordinates[vtx] = [p1.x, p1.y];
            } else if (shapeType === 'Rectangle') {
                shape.geometry.coordinates[vtx] = [p1.x, p1.y];
            }
            // remove move/end handler
            $document.off("mousemove.dlVertexDrag", dragMove);
            $document.off("mouseup.dlVertexDrag", dragEnd);
            // rearm start handler
            $handle.one("mousedown.dlVertexDrag", dragStart);
            if (onComplete != null) {
                onComplete(shape);
            } else {
                $(data).trigger('changeShape', shape);
            }
            return false;
        };

        // return drag start handler
        return dragStart;
    };
    
    /** 
     * define a shape by click and drag.
     */
    var defineShape = function(data, shape, onComplete) {
        var shapeType = shape.geometry.type;
        var $elem = data.$elem;
        var $body = $('body');
        var bodyRect = geom.rectangle($body);
        // overlay div prevents other elements from reacting to mouse events 
        var $overlayDiv = $('<div class="'+data.settings.cssPrefix+'shapeOverlay" style="position:absolute; z-index:100;"/>');
        $elem.append($overlayDiv);
        bodyRect.adjustDiv($overlayDiv);
        
        var shapeStart = function (evt) {
            var pt = geom.position(evt);
            // setup shape
            var p = data.imgTrafo.invtransform(pt);
            if (shapeType === 'Line' || shapeType === 'Rectangle') {
                shape.geometry.coordinates = [[p.x, p.y], [p.x, p.y]];
            } else {
                console.error("unsupported shape type: "+shapeType);
                $overlayDiv.remove();
                return false;
            }
            // draw shape
            renderShape(data, shape);
            // execute vertex drag handler on second vertex
            getVertexDragHandler(data, shape, 1, function (newshape) {
                // dragging vertex done
                console.debug("new shape:", shape);
                data.shapes.push(shape);
                $overlayDiv.remove();
                if (onComplete != null) {
                    onComplete(data, shape);
                }
            })(evt);
            return false;
        };
        
        // start by clicking
        $overlayDiv.one('mousedown.dlShape', shapeStart);
    };
    
    var handleUpdate = function (evt) {
        console.debug("vector: handleUpdate");
        var data = this;
        if (data.shapes == null || data.imgTrafo == null || !data.settings.isVectorActive)
            return;
        if (data.imgTrafo != data.vectorOldImgTrafo) {
            // imgTrafo changed
            renderShapes(data);
            data.vectorOldImgTrafo = data.imgTrafo;
        }
        //data.$svg.show();
    };

    /**
     * create a SVG element
     */
    var createSvg = function (name, attrs) {
        var elem = document.createElementNS(svgNS, name);
        if (attrs != null) {
            for (var att in attrs) {
                elem.setAttributeNS(null, att, attrs[att]);
            };
        }
        return elem;
    };
    
    // plugin object, containing name, install and init routines 
    // all shared objects are filled by digilib on registration
    var plugin = {
            name : 'vector',
            install : install,
            init : init,
            buttons : {},
            actions : {},
            fn : {},
            plugins : {}
    };

    if ($.fn.digilib == null) {
        $.error("jquery.digilib.vector.js must be loaded after jquery.digilib!");
    } else {
        $.fn.digilib('plugin', plugin);
    }
})(jQuery);