Mercurial > hg > digilib
view webapp/src/main/webapp/jquery/jquery.digilib.vector.js @ 1438:b8f2fde0f034 release-2.3
Merge with 1edc05d955cba1bd06c340b60650f507be4af755
author | robcast |
---|---|
date | Tue, 10 Nov 2015 14:30:43 +0100 |
parents | c7fb7b895e95 |
children | dc63c24c59be |
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. * Currently supported types: "Point", "Line", "LineString", "Rectangle", "Polygon", "Circle". * coordinates is a list of pairs of relative coordinates. * properties are the SVG properties "stroke", "stroke-width", "fill" and other properties. * A property 'editable':true will display drag-handles to change the shape. * Editing the shape will send a "changeShape"(shape) event. * 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', * 'editable' : true * } * } * */ (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 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 all existing shapes on layer. * * @param data * @param shapes * @param layer */ setShapes : function (data, shapes, layer) { if (layer == null) { // assume shape layer is 0 layer = data.vectorLayers[0]; } layer.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'}}. * onComplete(data, newShape) will be called when done. * * @param data * @param shape * @param onComplete * @param layer */ addShape : function (data, shape, onComplete, layer) { if (layer == null) { // assume shape layer is 0 layer = data.vectorLayers[0]; } if (layer.shapes == null) { layer.shapes = []; } if (shape.geometry.coordinates == null) { // define shape interactively defineShape(data, shape, layer, onComplete); } else { layer.shapes.push(shape); renderShapes(data, layer); } }, /** * get vector object (shape) by id. * * @param data * @param id * @returns shape */ getShapeById : function (data, id, layer) { if (layer == null) { // assume shape layer is 0 layer = data.vectorLayers[0]; } var shapes = layer.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, layer) { if (layer == null) { // assume shape layer is 0 layer = data.vectorLayers[0]; } var shapes = layer.shapes; if (shapes == null) return; for (var i = 0; i < shapes.length; ++i) { if (shapes[i].id === id) { shapes.splice(i, 1); } } renderShapes(data, layer); }, /** * add vector layer. * * Layer is an object with a "projection" member. * projection can be "relative": relative (0..1) coordinates, * "screen": on-screen coordinates (needs renderFn(data, layer)). * A SVG layer is specified by the jQuery-HTML element "$elem" and the SVG-element "svgElem". * * layer : { * projection : relative, * $elem : $(...), * svgElem : ... * } * * @param date * @param layer */ addVectorLayer : function (data, layer) { if (layer.projection === 'relative') { var svg = layer.svgElem; // set defaults for SVG in relative coordinates svg.setAttributeNS(null, 'viewBox', '0 0 1 1'); svg.setAttributeNS(null, 'preserveAspectRatio', 'none'); var $elem = layer.$elem; // set defaults for HTML element $elem.css({'position':'absolute', 'z-index': 9, 'pointer-events':'none'}); $elem.addClass(data.settings.cssPrefix+'overlay'); } // add layer data.vectorLayers.push(layer); renderLayers(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); // export functions digilib.fn.vectorDefaultRenderFn = renderShapes; digilib.fn.svgElement = svgElement; }; /** * plugin initialization */ var init = function (data) { console.debug('initialising vector plugin. data:', data); var $data = $(data); // create default shapes layer var shapeLayer = { 'projection': 'screen', 'renderFn': renderShapes, 'shapes': [] }; // shapes layer is first data.vectorLayers = [shapeLayer]; // pluggable SVG create functions data.shapeFactory = getShapeFactory(data); setupHandleFactory(data); // install event handlers $data.bind('update', handleUpdate); }; /** * handle update event */ var handleUpdate = function (evt) { console.debug("vector: handleUpdate"); var data = this; if (data.imgTrafo == null || !data.settings.isVectorActive) return; if (data.imgTrafo != data._vectorImgTrafo) { // imgTrafo changed -- redraw renderLayers(data); // save new imgTrafo data._vectorImgTrafo = data.imgTrafo; } }; /** * render all layers on screen */ var renderLayers = function (data) { if (data.imgRect == null) return; for (var i in data.vectorLayers) { var layer = data.vectorLayers[i]; if (layer.projection === 'screen') { // screen layers have render function if (layer.renderFn == null) { // user renderShapes as default layer.renderFn = renderShapes; } layer.renderFn(data, layer); } else if (layer.projection === 'relative') { var svg = layer.svgElem; if (svg != null) { // set current viewBox (jQuery lowercases attributes) svg.setAttribute('viewBox', data.zoomArea.getAsSvg()); } var $elem = layer.$elem; if ($elem != null) { // adjust layer element size and position (doesn't work with .adjustDiv()) $elem.css(data.imgRect.getAsCss()); $elem.show(); } } } }; /** * render all shapes on the layer. * * @param data * @param layer */ var renderShapes = function (data, layer) { if (layer == null) { // assume shape layer is 0 layer = data.vectorLayers[0]; } var shapes = layer.shapes || data.shapes; if (shapes == null || data.imgTrafo == null || !data.settings.isVectorActive) return; // set up shapes for (var i = 0; i < shapes.length; ++i) { var shape = shapes[i]; data.shapeFactory[shape.geometry.type].setup(data, shape); } // sort shapes by size descending shapes.sort(function (a, b) { return (b.properties.sorta - a.properties.sorta); }); // set up SVG var $svg = layer.$elem; if ($svg != null) { $svg.remove(); } var svgElem = svgElement('svg', { 'viewBox': data.imgRect.getAsSvg(), 'class': data.settings.cssPrefix+'overlay', 'style': 'position:absolute; z-index:10; pointer-events:none;'}); $svg = $(svgElem); layer.svgElem = svgElem; layer.$elem = $svg; for (var i = 0; i < shapes.length; ++i) { var shape = shapes[i]; renderShape(data, shape, layer); } data.$elem.append($svg); // adjust layer element size and position (doesn't work with .adjustDiv()) $svg.css(data.imgRect.getAsCss()); $svg.show(); }; /** * setup Shape SVG creation functions * (more functions can be plugged into data.settings.ShapeFactory) * * @param data */ var getShapeFactory = function (data) { var settings = data.settings; var css = settings.cssPrefix; var hs = settings.editHandleSize; // set standard SVG attributes var svgAttr = function (shape) { var props = shape.properties; return { 'id': shape.id || digilib.fn.createId(shape.id, css+'svg-'), 'stroke': props['stroke'] || settings.defaultStroke, 'stroke-width' : props['stroke-width'] || settings.defaultStrokeWidth, 'fill' : props['fill'] || settings.defaultFill, 'class' : props['cssclass'], 'style' : props['style'] }; }; var factory = { 'Point' : { 'setup' : function (data, shape) { if (shape.properties == null) shape.properties = {}; shape.properties.maxvtx = 1; shape.properties.sorta = 0; }, 'svg' : function (shape) { var $s = $(svgElement('path', svgAttr(shape))); $s.place = function () { // point uses pin-like path of size 3*pu var p = shape.properties.screenpos[0]; var pu = hs / 3; this.attr({'d': 'M '+p.x+','+p.y+' l '+2*pu+','+pu+' c '+2*pu+','+pu+' '+0+','+3*pu+' '+(-pu)+','+pu+' Z'}); }; return $s; } }, 'Line' : { 'setup' : function (data, shape) { if (shape.properties == null) shape.properties = {}; shape.properties.maxvtx = 2; shape.properties.bbox = getBboxRect(data, shape); shape.properties.sorta = 0; }, 'svg' : function (shape) { var $s = $(svgElement('line', svgAttr(shape))); $s.place = function () { var p = shape.properties.screenpos; this.attr({'x1': p[0].x, 'y1': p[0].y, 'x2': p[1].x, 'y2': p[1].y}); }; return $s; } }, 'Rectangle' : { 'setup' : function (data, shape) { if (shape.properties == null) shape.properties = {}; shape.properties.maxvtx = 2; shape.properties.bbox = getBboxRect(data, shape); if (shape.properties.bbox != null) { shape.properties.sorta = shape.properties.bbox.getArea(); } }, 'svg' : function (shape) { var $s = $(svgElement('rect', svgAttr(shape))); $s.place = function () { var p = shape.properties.screenpos; var r = geom.rectangle(p[0], p[1]); this.attr({'x': r.x, 'y': r.y, 'width': r.width, 'height': r.height}); }; return $s; } }, 'Polygon' : { 'setup' : function (data, shape) { if (shape.properties == null) shape.properties = {}; shape.properties.bbox = getBboxRect(data, shape); if (shape.properties.bbox != null) { shape.properties.sorta = shape.properties.bbox.getArea(); } }, 'svg' : function (shape) { var $s = $(svgElement('polygon', svgAttr(shape))); $s.place = function () { var p = shape.properties.screenpos; this.attr({'points': p.join(" ")}); }; return $s; } }, 'LineString' : { 'setup' : function (data, shape) { if (shape.properties == null) shape.properties = {}; shape.properties.bbox = getBboxRect(data, shape); if (shape.properties.bbox != null) { shape.properties.sorta = shape.properties.bbox.getArea(); } }, 'svg' : function (shape) { var $s = $(svgElement('polyline', svgAttr(shape))); $s.place = function () { var p = shape.properties.screenpos; this.attr({'points': p.join(" ")}); }; return $s; } }, 'Circle' : { 'setup' : function (data, shape) { if (shape.properties == null) shape.properties = {}; shape.properties.maxvtx = 2; // TODO: bbox not really accurate shape.properties.bbox = getBboxRect(data, shape); if (shape.properties.bbox != null) { shape.properties.sorta = shape.properties.bbox.getArea(); } }, 'svg' : function (shape) { var $s = $(svgElement('circle', svgAttr(shape))); $s.place = function () { var p = shape.properties.screenpos; this.attr({'cx': p[0].x, 'cy': p[0].y, 'r': p[0].distance(p[1])}); }; return $s; } }, 'Ellipse' : { 'setup' : function (data, shape) { if (shape.properties == null) shape.properties = {}; shape.properties.maxvtx = 2; // TODO: bbox not really accurate shape.properties.bbox = getBboxRect(data, shape); if (shape.properties.bbox != null) { shape.properties.sorta = shape.properties.bbox.getArea(); } }, 'svg' : function (shape) { var $s = $(svgElement('ellipse', svgAttr(shape))); $s.place = function () { var p = shape.properties.screenpos; this.attr({'cx': p[0].x, 'cy': p[0].y, 'rx' : Math.abs(p[0].x - p[1].x), 'ry' : Math.abs(p[0].y - p[1].y)}); }; return $s; } } }; return factory; }; /** * setup handle creation functions * (more functions can be plugged into data.settings.handleFactory) * * @param data */ var setupHandleFactory = function (data) { var settings = data.settings; var css = settings.cssPrefix; var hs = settings.editHandleSize; var d = hs/2; var attr = { 'stroke': 'darkgrey', 'stroke-width': 1, 'fill': 'none', 'class': css+'svg-handle', 'style': 'pointer-events:all' }; var factory = { 'square' : function () { var $h = $(svgElement('rect', attr)); $h.attr({'width': hs, 'height': hs}); $h.moveTo = function (p) { this.attr({'x': p.x-d, 'y': p.y-d }); }; return $h; }, 'diamond' : function () { var $h = $(svgElement('polygon', attr)); $h.moveTo = function (p) { this.attr('points', (p.x-d) +','+ p.y+ ' '+p.x +','+(p.y+d)+' '+(p.x+d)+','+p.y+' '+p.x+','+(p.y-d)); }; return $h; }, 'cross' : function () { var $h = $(svgElement('path', attr)); $h.moveTo = function (p) { this.attr('d', 'M'+(p.x-d) +','+ p.y+ ' L'+(p.x+d)+','+p.y+' M'+p.x+','+(p.y+d)+' L'+p.x+','+(p.y-d)); }; return $h; } }; data.handleFactory = factory; }; /** * create handles for a shape. * * Creates SVG elements for each screen point and append it to the SVG element. * * @param data * @param shape * @param svg The SVG element where to append handle elements * @param func If present, use a special create function */ //create handles for a shape. var createHandles = function (data, shape, layer) { if (!shape.properties.editable) { return }; var $svg = $(layer.svgElem); var trafo = data.imgTrafo; // type of handle can be stated in layer var type = layer.handleType; var newHandle = data.handleFactory[type] || data.handleFactory['square']; var handles = []; var createHandle = function (i, item) { var p = trafo.transform(geom.position(item)); var $handle = newHandle(); $handle.moveTo(p); handles.push($handle); $svg.append($handle); return $handle; }; var coords = shape.geometry.coordinates; $.each(coords, createHandle); // vertexElems must be defined before calling getVertexDragHandler() shape.$vertexElems = handles; var done = function (data, shape, evt) { unrenderShape(data, shape); renderShape(data, shape, layer); } var attachEvent = function (i, item) { item.one("mousedown.dlVertexDrag", getVertexDragHandler(data, shape, i, done)); }; $.each(handles, attachEvent); }; /** * calculate screen positions from coordinates for a shape. * * @param data * @param shape */ var createScreenCoords = function (data, shape) { var coords = shape.geometry.coordinates; var trafo = data.imgTrafo; var screenpos = $.map(coords, function (coord) { return trafo.transform(geom.position(coord)); }); shape.properties.screenpos = screenpos; return screenpos; }; var getBboxRect = function (data, shape) { var coords = shape.geometry.coordinates; if (coords == null) return null; var xmin = 1; var xmax = 0; var ymin = 1; var ymax = 0; var x, y; for (var i = 0; i < coords.length; ++i) { x = coords[i][0]; y = coords[i][1]; xmin = (x < xmin) ? x : xmin; xmax = (x > xmax) ? x : xmax; ymin = (y < ymin) ? y : ymin; ymax = (y > ymax) ? y : ymax; } return geom.rectangle(xmin, ymin, xmax-xmin, ymax-ymin); }; /** * render a shape on screen. * * Creates a SVG element and adds it to the layer. * Puts a reference $elem in the shape object. * * @param data * @param shape * @param layer */ var renderShape = function (data, shape, layer) { // make sure we have a SVG element if (layer.svgElem == null) { renderShapes(data, layer); return; } var shapeType = shape.geometry.type; if (!isSupported(data, shapeType)) { console.error("renderShape: unsupported shape type: "+shapeType); return; } // create the SVG var $elem = data.shapeFactory[shapeType].svg(shape); shape.$elem = $elem; // place the SVG on screen createScreenCoords(data, shape); $elem.place(); // render the SVG $(layer.svgElem).append($elem); createHandles(data, shape, layer); $(data).trigger("renderShape", shape); }; /** * remove rendered shape from screen. * * Removes the SVG elements from the layer. * * @param data * @param shape */ var unrenderShape = function (data, shape) { // remove vertex handles if (shape.$vertexElems != null) { for (var i = 0; i < shape.$vertexElems.length; ++i) { shape.$vertexElems[i].remove(); } delete shape.$vertexElems; } // remove SVG element if (shape.$elem != null) { shape.$elem.remove(); delete shape.$elem; } }; /** * return a vertexDragHandler function. * * @param data * @param shape shape to drag * @param vtx vertex number on shape * @onComplete function (data, shape) */ var getVertexDragHandler = function (data, shape, vtx, onComplete) { var $document = $(document); var imgRect = data.imgRect; var $shape = shape.$elem; var $handle = (shape.$vertexElems != null) ? shape.$vertexElems[vtx] : null; var shapeType = shape.geometry.type; var props = shape.properties; var pos = props.screenpos; var pStart; // save startpoint var placeHandle = function (i, $handle) { $handle.moveTo(pos[i]); }; var placeHandles = function () { $.each(shape.$vertexElems, placeHandle); }; var dragStart = function (evt) { // start dragging // cancel if not left-click if (evt.which != 1) return; pStart = geom.position(evt); props.startpos = pStart; props.vtx = vtx; $(data).trigger('positionShape', shape); $document.on("mousemove.dlVertexDrag", dragMove); $document.on("mouseup.dlVertexDrag", dragEnd); $document.on("dblclick.dlVertexDrag", dragEnd); return false; }; var dragMove = function (evt) { // dragging var pt = geom.position(evt); pt.clipTo(imgRect); pos[vtx].moveTo(pt); if (isSupported(data, shapeType)) { // trigger drag event (may manipulate screen position) $(data).trigger('positionShape', shape); // update vertex coords of shape shape.geometry.coordinates[vtx] = data.imgTrafo.invtransform(pos[vtx]).toArray(); // update shape SVG element $shape.place(); // move handles accordingly if (shape.$vertexElems != null) placeHandles(); $(data).trigger('dragShape', shape); } return false; }; var dragEnd = function (evt) { // end dragging var pt = geom.position(evt); if ((pt.distance(pStart) < 5) && evt.type === 'mouseup') { // not drag but click to start return false; } dragMove(evt); // remove move/end handler $document.off("mousemove.dlVertexDrag", dragMove); $document.off("mouseup.dlVertexDrag", dragEnd); $document.off("dblclick.dlVertexDrag", dragEnd); // call setup to update bbox data.shapeFactory[shapeType].setup(data, shape); // rearm start handler if ($handle != null) { $handle.one("mousedown.dlVertexDrag", dragStart); } if (onComplete != null) { onComplete(data, shape, evt); } else { $(data).trigger('changeShape', shape); } return false; }; // return drag start handler return dragStart; }; /** * returns true if shapeType is supported * * @param shapeType shapeType to test */ var isSupported = function (data, shapeType) { return data.shapeFactory[shapeType] != null; }; /** * define a shape by click and drag. * * The given shape object has to have a type, but its coordinates will be overwritten. * * @param data * @param shape the shape to define * @param layer the layer to draw on * @onComplete function (data, shape) */ var defineShape = function (data, shape, layer, onComplete) { if (layer == null) { // assume shape layer is 0 layer = data.vectorLayers[0]; } var shapeType = shape.geometry.type; // call setup to make sure maxvtx is set data.shapeFactory[shapeType].setup(data, shape); 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 p1 = data.imgTrafo.invtransform(pt).toArray(); var p2 = p1.slice(0); var vtx = 1; if (shapeType === 'Point') { shape.geometry.coordinates = [p1]; } else if (isSupported(data, shapeType)) { shape.geometry.coordinates = [p1, p2]; } else { console.error("defineShape: unsupported shape type: "+shapeType); $overlayDiv.remove(); return false; } // shape is not editable by default if (shape.properties == null) { shape.properties = {'editable' : false}; } // save first mousedown position shape.properties.screenpos = [pt]; shape.properties.vtx = vtx; // draw shape renderShape(data, shape, layer); // vertex drag end handler var vertexDragDone = function (data, shape, evt) { var coords = shape.geometry.coordinates; var max = shape.properties.maxvtx; if (max == null || vtx < max-1) { // multipoint shape (e. g. Polygon, LineString) if (evt.type === 'mouseup') { // single click adds next point unrenderShape(data, shape); // copy last vertex as starting point coords.push(coords[vtx].slice()); vtx += 1; // draw shape shape.properties.vtx = vtx; renderShape(data, shape, layer); // execute vertex drag handler on next vertex getVertexDragHandler(data, shape, vtx, vertexDragDone)(evt); return false; } else if (evt.type === 'dblclick') { // double click ends multipoint shape var rerender = false; // remove duplicate vertices (from mouseup) while (coords[vtx][0] === coords[vtx-1][0] && coords[vtx][1] === coords[vtx-1][1]) { coords.pop(); vtx -= 1; rerender = true; } if (rerender) { unrenderShape(data, shape); shape.properties.vtx = vtx; renderShape(data, shape, layer); } } else { console.error("unknown event type!"); return false; } } shapeDone(data, shape); }; if (vtx === shape.properties.maxvtx) { // last vertex shapeDone(data, shape); } else { // execute vertex drag handler on next vertex getVertexDragHandler(data, shape, vtx, vertexDragDone)(evt); } return false; }; var shapeDone = function (data, shape) { // defining shape done unrenderShape(data, shape); // call setup to update bbox data.shapeFactory[shapeType].setup(data, shape); renderShape(data, shape, layer); // save shape layer.shapes.push(shape); $overlayDiv.remove(); if (onComplete != null) { onComplete(data, shape); } }; // start by clicking $overlayDiv.one('mousedown.dlShape', shapeStart); }; /** * create a SVG element with attributes. * * @param name tag name * @param attrs object with attributes */ var svgElement = function (name, attrs) { var elem = document.createElementNS(svgNS, name); if (attrs != null) { for (var att in attrs) { if (attrs[att] != null) { 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);