view webapp/src/main/webapp/jquery/jquery.digilib.measure.js @ 1610:f425f00bf5e3 geom_prototype

changed digilib.geometry to use new and prototypes instead of adding methods to objects.
author robcast
date Sun, 12 Mar 2017 15:53:01 +0100
parents 020739227ffd
children
line wrap: on
line source

/*
 * #%L
 * digilib measure plugin
 * %%
 * Copyright (C) 2012 - 2014 Bibliotheca Hertziana, MPIWG Berlin
 * %%
 * 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: Martin Raspe, Robert Casties, 2012-2014
 */
/**
 * digilib measure plugin (measure distances on the digilib image in historic units etc.)
**/ 

/* jslint browser: true, debug: true, forin: true
*/

/* TODO:
    - infowindow for shapes (partially done)
        - display fractions (1/3 etc.)
        - display angles
        - display Vitruvian intercolumnium types
    - display shapes overlay? (angles, distances?)
    - move shapes (shift+drag?)
    - confine oval to sensible measurements
    - better grid
    - snap vertex to next "round" unit / sub-unit while dragging (on keypress?)
*/


(function($) {

    // the digilib object
    var digilib = null;
    // the normal zoom area
    var FULL_AREA = null;

    // the functions made available by digilib
    var fn = {
        // dummy function to avoid errors, gets overwritten by buttons plugin
        highlightButtons : function () {
            console.debug('measure: dummy function - highlightButtons');
            }
        };
    // affine geometry plugin
    var geom = null;
    // convenience variable, set in init()
    var CSS = '';
    // shape currently being drawn/dragged
    var currentShape;
    // current key state: keystate[event.key] = event
    var keystate = {};
    // shiftPressed = event.shiftKey;
    // altPressed   = event.altKey;
    // ctrlPressed  = event.ctrlKey;

    // the conversion data
    var UNITS = {
        comment : [
          "Angaben nach:",
          "Klimpert, Richard: Lexikon der Münzen, Maße, Gewichte, Zählarten und Zeitgrößen aller Länder der Erde 2) Berlin 1896 (Reprint Graz 1972)",
          "Doursther, Horace: Dictionnaire universel des poids et mesures anciens et modernes. Paris 1840 (Reprint Amsterdam 1965)"
          ],
        sections : [{
            name  : "Längenmaße: metrisch",
            group  : "1",
            units  : [{
				name : "m",
				factor : "1"
				},
				{
				name : "mm",
				factor : "0.001"
				},
				{
				name : "cm",
				factor : "0.01"
				},
				{
				name : "dm",
				factor : "0.1"
				},
				{
				name : "km",
				factor : "1000"
				}]
            },
            {
            name  : "Längenmaße: nautisch",
            group  : "1",
            units  : [{
				name : "geographische Meile",
				factor : "7420"
				},
				{
				name : "Seemeile",
				factor : "1854.965"
				},
				{
				name : "fathom",
				factor : "1.828782"
				},
				{
				name : "cable",
				factor : "182.8782"
				},
				{
				name : "league",
				factor : "5564.895"
				}]
            },
            {
            name  : "Längenmaße: England",
            group  : "1",
            units  : [{
				name : "foot",
				factor : "0.304797",
				subunits : "12"
				},
				{
				name : "inch",
				factor : "0.02539975"
				},
				{
				name : "yard",
				factor : "0.914391",
				subunits : "3"
				},
				{
				name : "pole",
				factor : "5.0291505",
				subunits : "11"
				},
				{
				name : "chain",
				factor : "20.116602",
				subunits : "4"
				},
				{
				name : "furlong",
				factor : "201.16602"
				},
				{
				name : "mile",
				factor : "1609.32816",
				subunits : "8"
				}]
            },
            {
            name  : "Längenmaße: Italien",
            group  : "1",
            units  : [{
				name : "palmo d'architetto (Rom)",
				factor : "0.223425",
				subunits : "12"
				},
				{
				name : "braccio (Florenz)",
				factor : "0.5836"
				},
				{
				name : "braccio (Mailand)",
				factor : "0.5949"
				},
				{
				name : "canna d'architetto (Rom)",
				factor : "2.23425"
				},
				{
				name : "canna di commercio (Rom)",
				factor : "1.9920"
				},
				{
				name : "canna d'architetto (Florenz)",
				factor : "2.9180"
				},
				{
				name : "canna di commercio (Florenz)",
				factor : "2.3344"
				},
				{
				name : "canna (Neapel)",
				factor : "2.0961"
				},
				{
				name : "miglio (Lombardei)",
				factor : "1784.808"
				},
				{
				name : "miglio (Neapel)",
				factor : "1855.110"
				},
				{
				name : "miglio (Rom)",
				factor : "1489.50"
				},
				{
				name : "minuta (Rom)",
				factor : "0.00372375"
				},
				{
				name : "oncia (Rom)",
				factor : "0.01861875"
				},
				{
				name : "oncia (Mailand)",
				factor : "0.49575"
				},
				{
				name : "palmo di commercio (Rom)",
				factor : "0.249"
				},
				{
				name : "palmo (Florenz)",
				factor : "0.2918"
				},
				{
				name : "piede (Brescia)",
				factor : "0.471"
				},
				{
				name : "piede (Carrara)",
				factor : "0.2933"
				},
				{
				name : "piede (Como)",
				factor : "0.4512"
				},
				{
				name : "piede (Modena)",
				factor : "0.523048"
				},
				{
				name : "piede (Reggio Em.)",
				factor : "0.530898"
				},
				{
				name : "piede (Venedig)",
				factor : "0.347735"
				},
				{
				name : "piede (Vicenza)",
				factor : "0.3574"
				},
				{
				name : "piede (Verona)",
				factor : "0.3429"
				},
				{
				name : "piede (Rom)",
				factor : "0.297587"
				},
				{
				name : "piede Lombardo",
				factor : "0.435185"
				},
				{
				name : "piede liprando (Turin)",
				factor : "0.51377"
				},
				{
				name : "piede manuale (Turin)",
				factor : "0.342511"
				},
				{
				name : "piede (Neapel, 'palmo')",
				factor : "0.26455"
				},
				{
				name : "soldo (Florenz)",
				factor : "0.2918"
				},
				{
				name : "trabucco piemontese (Turin)",
				factor : "3.08259"
				}]
            },
            {
            name  : "Längenmaße: Niederlande",
            group  : "1",
            units  : [{
				name : "voet (Amsterdam)",
				factor : "0.283113"
				},
				{
				name : "voet (Antwerpen)",
				factor : "0.2868"
				},
				{
				name : "voet (Aelst)",
				factor : "0.2772"
				},
				{
				name : "voet (Breda)",
				factor : "0.28413"
				},
				{
				name : "voet (Brügge)",
				factor : "0.27439"
				},
				{
				name : "voet (Brüssel)",
				factor : "0.2757503"
				},
				{
				name : "voet (Groningen)",
				factor : "0.2922"
				},
				{
				name : "voet (Haarlem)",
				factor : "0.2858"
				},
				{
				name : "voet (Kortrijk)",
				factor : "0.2977"
				},
				{
				name : "voet (Tournai)",
				factor : "0.2977"
				},
				{
				name : "voet (Utrecht)",
				factor : "0.2683"
				},
				{
				name : "voet (Ypern)",
				factor : "0.2739"
				},
				{
				name : "pied (Hainaut)",
				factor : "0.2934"
				},
				{
				name : "pied St. Hubert (Lüttich)",
				factor : "0.294698"
				},
				{
				name : "pied St. Lambert (Lüttich)",
				factor : "0.291796"
				},
				{
				name : "pied Ste. Gertrude (Nivelles)",
				factor : "0.27709"
				},
				{
				name : "steenvoet (Oudenaerde)",
				factor : "0.2977"
				},
				{
				name : "houtvoet (Oudenaerde)",
				factor : "0.292"
				}]
            },
            {
            name  : "Längenmaße: Frankreich",
            group  : "1",
            units  : [{
				name : "pied du Roi (Paris)",
				factor : "0.32483938497"
				},
				{
				name : "pied (Arras)",
				factor : "0.29777"
				},
				{
				name : "pied (Cambrai)",
				factor : "0.29777"
				},
				{
				name : "Burgundischer Fuß",
				factor : "0.33212"
				}]
            },
            {
            name  : "Längenmaße: Südeuropa",
            group  : "1",
            units  : [{
				name : "pié de Burgos (Spanien)",
				factor : "0.278635"
				},
				{
				name : "pé (Portugal)",
				factor : "0.33"
				}]
            },
            {
            name  : "Längenmaße: deutschspr. Länder",
            group  : "1",
            units  : [{
				name : "Fuß (Basel)",
				factor : "0.29820"
				},
				{
				name : "Fuß (Bayern)",
				factor : "0.2918592"
				},
				{
				name : "Fuß (Braunschweig)",
				factor : "0.2853624"
				},
				{
				name : "Fuß (Gotha)",
				factor : "0.287622"
				},
				{
				name : "Fuß (Hamburg)",
				factor : "0.286575"
				},
				{
				name : "Fuß (Hessen)",
				factor : "0.287669"
				},
				{
				name : "Fuß (Köln)",
				factor : "0.2876"
				},
				{
				name : "Fuß (Mecklenburg)",
				factor : "0.291006"
				},
				{
				name : "Fuß (Münster)",
				factor : "0.2908"
				},
				{
				name : "Fuß (Pommern)",
				factor : "0.2921"
				},
				{
				name : "Fuß (rheinisch)",
				factor : "0.3138535"
				},
				{
				name : "Fuß (Sachsen)",
				factor : "0.2831901"
				},
				{
				name : "Fuß (Preußen)",
				factor : "0.3138535"
				},
				{
				name : "Fuß (Wien)",
				factor : "0.3180807"
				},
				{
				name : "Fuß (Württemberg)",
				factor : "0.2864903"
				},
				{
				name : "Werkschuh (Frankfurt)",
				factor : "0.2846143"
				},
				{
				name : "Meile (Preußen)",
				factor : "7532.485"
				},
				{
				name : "Postmeile (Österreich)",
				factor : "7585.937"
				},
				{
				name : "Dezimalfuß (Preußen)",
				factor : "0.3766242"
				}]
            },
            {
            name  : "Längenmaße: Osteuropa",
            group  : "1",
            units  : [{
				name : "Fuß (Böhmen)",
				factor : "0.2964"
				},
				{
				name : "Fuß (Mähren)",
				factor : "0.29596"
				},
				{
				name : "stopa (Krakauer Fuß)",
				factor : "0.3564"
				},
				{
				name : "stopa (Warschauer Fuß)",
				factor : "0.288"
				},
				{
				name : "Fuß (Rußland)",
				factor : "0.3556"
				},
				{
				name : "arschin",
				factor : "0.7112"
				},
				{
				name : "saschen (Faden)",
				factor : "2.133"
				},
				{
				name : "werst",
				factor : "1066.8"
				},
				{
				name : "milja",
				factor : "7468"
				}]
            },
            {
            name  : "Längenmaße: Antike",
            group  : "1",
            units  : [{
				name : "pes romanus",
				factor : "0.2945"
				},
				{
				name : "pollex (Zoll)",
				factor : "0.0245416667"
				},
				{
				name : "digitus (Fingerbreite)",
				factor : "0.01840625"
				},
				{
				name : "palmus (Handbreite)",
				factor : "0.073625"
				},
				{
				name : "cubitus (Elle)",
				factor : "0.44175"
				},
				{
				name : "passus (Doppelschritt)",
				factor : "1.4725"
				},
				{
				name : "pertica",
				factor : "2.945"
				},
				{
				name : "actus",
				factor : "35.34"
				},
				{
				name : "mille passus (Meile)",
				factor : "1472.5"
				},
				{
				name : "stadium (600 Fuß)",
				factor : "176.7"
				},
				{
				name : "stadium (1/8 Meile)",
				factor : "184.0625"
				},
				{
				name : "stadion (Olympia)",
				factor : "192.25"
				},
				{
				name : "Fuß (attisch)",
				factor : "0.308"
				},
				{
				name : "Fuß (Babylon)",
				factor : "0.35"
				},
				{
				name : "Fuß (Delphi)",
				factor : "0.1848"
				},
				{
				name : "Fuß (Olympia)",
				factor : "0.32041667"
				}]
            },
            {
            name  : "Fläche",
            group  : "4",
            units  : [{
				name : "qm",
				factor : "1"
				},
				{
				name : "qmm",
				factor : "0.000001"
				},
				{
				name : "qcm",
				factor : "0.0001"
				},
				{
				name : "qdm",
				factor : "0.01"
				},
				{
				name : "Ar",
				factor : "100"
				},
				{
				name : "Morgen",
				factor : "2500"
				},
				{
				name : "Hektar",
				factor : "10000"
				},
				{
				name : "qkm",
				factor : "1000000"
				},
				{
				name : "square inch",
				factor : "0.0006452"
				},
				{
				name : "square foot",
				factor : "0.09288"
				},
				{
				name : "square yard",
				factor : "0.836",
				subunits : "9"
				},
				{
				name : "pole (rod, perch)",
				factor : "25.289"
				},
				{
				name : "rood",
				factor : "1012",
				subunits : "40"
				},
				{
				name : "acre",
				factor : "4048",
				subunits : "4"
				},
				{
				name : "square mile",
				factor : "2590000"
				}]
            },
            {
            name  : "Sonstige",
            group  : "0",
            units  : [{
				name : "Maßstab 1:200",
				factor : "200"
				},
				{
				name : "Maßstab",
				factor : "1:100",
				add : "100"
				},
				{
				name : "Maßstab 1:75",
				factor : "75"
				},
				{
				name : "Maßstab 1:60",
				factor : "60"
				},
				{
				name : "Maßstab",
				factor : "1:50",
				add : "50"
				},
				{
				name : "Maßstab 1:25",
				factor : "25"
				},
				{
				name : "Maßstab 1:20",
				factor : "20"
				},
				{
				name : "Maßstab 1:10",
				factor : "10"
				},
				{
				name : "Maßstab 1:5",
				factor : "5"
				},
				{
				name : "Maßstab 1:3",
				factor : "3"
				}]
          }]
        };
    var buttons = {
        measure : {
            onclick : "measurebar",
            tooltip : "show the measuring toolbar",
            icon : "measure.png"
            },
        drawshape : {
            onclick : "drawshape",
            tooltip : "draw a shape",
            }
        };

    var defaults = {
        // buttonset of this plugin
        measureButtonSet : ['measurebar'],
        // unit data
        units : UNITS,
        // styles for shapes
        styles : {
            shape : {
                stroke : 'lightgreen',
                'stroke-width' : 2,
                fill : 'none'
                },
            constr : {
                stroke : 'cornsilk',
                'stroke-width' : 1,
                fill : 'none'
                },
            guide : {
                stroke : 'blue',
                'stroke-width' : 1,
                fill : 'none'
                },
            selected : {
                stroke : 'cyan',
                'stroke-width' : 3,
                fill : 'none'
                },
            handle : {
                stroke : 'blue',
                'stroke-width' : 1,
                fill : 'none',
                hover : {
                    fill : 'yellow',
                    }
                }
            },
        // implemented styles
        implementedStyles : ['shape', 'constr', 'guide', 'selected', 'handle'],
        // implemented measuring shape types, for select widget
        implementedShapes : ['Line', 'LineString', 'Proportion', 'Rect', 'Rectangle', 'Polygon', 'Circle', 'Ellipse', 'Intercolumnium', 'Oval', 'Grid'],
        // all measuring shape types
        shapeInfo : {
            Line:           { name : 'line',           display : 'length', },
            LineString:     { name : 'linestring',     display : 'length'  },
            Proportion:     { name : 'proportion',     display : 'length'  },
            Rectangle:      { name : 'box',            display : 'diagonal' },
            Rect:           { name : 'rectangle',      display : 'area'    },
            Square:         { name : 'square',         display : 'length'  },
            Polygon:        { name : 'polygon',        display : 'area'    },
            Circle:         { name : 'circle',         display : 'radius'  },
            Ellipse:        { name : 'ellipse',        display : 'area'    },
            Intercolumnium: { name : 'intercolumnium', display : 'distance' },
            Oval:           { name : 'oval',           display : 'distance' },
            Grid:           { name : 'linegrid',       display : 'spacing' }
            },
        // currently selected shape type
        activeShapeType : 'Line',
        // last measured distance
        lastMeasuredValue : 0,
        // measuring unit (index into unit list)
        unitFrom : 17,
        // converted unit (index into unit list)
        unitTo : 0,
        // maximal denominator for mixed fractions
        maxDenominator : 20,
        // number of decimal places for convert results
        maxDecimals : 3,
        // show convert result as mixed fraction?
        showMixedFraction : false,
        // show angle relative to last line?
        showRelativeAngle : false,
        // show distance numbers?
        showDistanceNumbers : true,
        // show ratio of rectangle sides?
        showRectangleRatios : false,
        // draw line ends as small crosses
        drawEndPoints : true,
        // draw mid points of lines
        drawMidPoints : false,
        // draw circle centers
        drawCenters : false,
        // draw rectangles from the diagonal and one point
        drawFromDiagonal : false,
        // draw circles from center
        drawFromCenter : true,
        // snap to endpoints
        snapEndPoints : false,
        // snap to mid points of lines
        snapMidPoints : false,
        // snap to circle centers
        snapCenters : false,
        // snap distance (in screen pixels)
        snapDistance : 5,
        // keep original object when moving/scaling/rotating
        keepOriginal : false,
        // number of copies when drawing grids
        gridCopies : 10,
        // info window
        infoDiv : null
        };

    // debug routine
    var _debug_shape = function (msg, shape) {
        // console.debug('measure: ' + msg, shape.geometry.type, shape.geometry.coordinates);
        return;
        };

    // plugin actions
    var actions = {
        measurebar : function(data) {
            var $measureBar = data.$measureBar;
            if ($measureBar == null) {
                $measureBar = setupMeasureBar(data);
				};
			$measureBar.toggle();
			var on = $measureBar.is(":visible");
			attachKeyHandlers(data, on);
			showSVG(data, on);
			return;
            },
        drawshape : function(data) {
            var shape = newShape(data);
            var layer = data.measureLayer;
            $(data).trigger('createShape', shape);
            digilib.actions.addShape(data, shape, layer, shapeCompleted);
            console.debug('drawshape', shape);
            _debug_shape('action drawshape', shape);
            }
        };

    // callback for vector.drawshape
    var shapeCompleted = function(data, shape) {
        _debug_shape('shapeCompleted', shape);
        data.measureWidgets.startb.removeClass('dl-drawing');
        if (shape == null || shape.geometry.coordinates == null) {
            return false; // do nothing if no line was produced
            };
        $(data).trigger('changeShape', shape); // update widgets
        return false;
        };

    // callback for vector.drawshape
    var onCreateShape = function(event, shape) {
        var data = this;
        data.measureWidgets.startb.addClass('dl-drawing');
        _debug_shape('onCreateShape', shape);
    };

    // event handler for positionShape
    var onPositionShape = function(event, shape) {
        var data = this;
        currentShape = shape;
        if (keystate['x'] != null) { // change only x-Dimension of mouse pointer
            manipulatePosition(shape, lockDimension('y'));
            }
        if (keystate['y'] != null) { // change only y-Dimension of mouse pointer
            manipulatePosition(shape, lockDimension('x'));
            }
        if (keystate['s'] != null) { // snap to next unit
            manipulatePosition(shape, snapToUnit(data));
            }
        // console.debug('onPositionShape', shape.properties.screenpos);
    };

    // event handler for dragShape
    var onDragShape = function(event, shape) {
        var data = this;
        updateInfo(data, shape);
        _debug_shape('onDragShape', shape);
    };

    // event handler for changeShape
    var onChangeShape = function(event, shape) {
        var data = this;
        // event handler for updating shape info
        updateInfo(data, shape);
        currentShape = null;
        _debug_shape('onChangeShape', shape);
    };

    // event handler for renderShape
    var onRenderShape = function(event, shape) {
        var data = this;
        var select = function(event) {
            selectShape(data, this, shape);
            updateInfo(data, shape);
            _debug_shape('onClick', shape);
            };
        var info = function(event) {
            showInfoDiv(event, data, shape);
            _debug_shape('showInfoDiv', shape);
            };
        var $elem = shape.$interactor || shape.$elem;
        $elem.on('mouseover.measureinfo', info);
        $elem.on('click.measureselect', select);
        _debug_shape('onRenderShape', shape);
        };

    // get last vertex before current one
    var getLastVertex = function(shape, vertex) {
        var props = shape.properties;
        var vtx = vertex == null ? props.vtx : vertex;
        return (vtx === 0) ? props.screenpos.length-1 : vtx-1;
        };

    // calculate the distance of a shape vertex to the last (in rectified digilib coords)
    var getVertexDistance = function(data, shape, vtx) {
        if (vtx == null) {
            vtx = shape.properties.vtx; }
        var coords = shape.geometry.coordinates;
        var last = getLastVertex(shape, vtx);
        // safely assume that screenpos and coords have equal length?
        var dist = fn.getDistance(data, new geom.Position(coords[last]), new geom.Position(coords[vtx]));
        return dist.rectified;
        };

    // calculate distance from current to last vertex (in rectified digilib coords)
    var rectifiedDist = function(data, shape) {
        var coords = shape.geometry.coordinates;
        var total = 0;
        if (shape.geometry.type === 'LineString') { // sum up distances
            for (vtx = 1; vtx < coords.length; vtx++) {
                total += getVertexDistance(data, shape, vtx);
                }
        } else {
            total = getVertexDistance(data, shape);
            }
        return total;
        };

    // calculate the area of a polygon (rectified digilib coords)
    var rectifiedArea = function(data, shape) {
        var ar = fn.getImgAspectRatio(data);
        var rectifyPoint = function (c) {
            return new geom.Position(ar * c[0], c[1]);
            };
        var coords = $.map(shape.geometry.coordinates, rectifyPoint);
         // formula for ellipse area
        if (shape.geometry.type === 'Ellipse') {
            return Math.abs((coords[0].x-coords[1].x) * (coords[0].y-coords[1].y) * Math.PI);
            }
        // algorithm for polygon area
        var area = 0;
        j = coords.length-1; // set j to the last vertex
        for (i = 0; i < coords.length; i++) {
            area += (coords[j].x + coords[i].x) * (coords[j].y - coords[i].y); 
            j = i;  // set j to the current vertex, i increments
            }
        return Math.abs(area/2);
        };

    // recalculate factor after a new value was entered into input element "value1"
    var changeFactor = function(data) {
        var widgets = data.measureWidgets;
        var val = parseFloat(widgets.value1.val());
        var fac = val / data.lastMeasuredValue;
        data.measureFactor = fac;
        updateUnits(data);
    };

    // convert measured value to second unit and display
    var updateMeasures = function(data, val, type) {
        var widgets = data.measureWidgets;
        var unit1 = parseFloat(widgets.unit1.val());
        var unit2 = parseFloat(widgets.unit2.val());
        var ratio = unit1 / unit2;
        var result = scaleValue(data, type, val, ratio);
        widgets.shape.val(type);
        widgets.value1.val(fn.cropFloatStr(val));
        widgets.value2.text(fn.cropFloatStr(result));
        };

    // scale
    var scaleValue = function(data, type, val, factor) {
        var scaleArea = data.settings.shapeInfo[type].display === 'area';
        var result = scaleArea
            ? val * factor * factor
            : val * factor;
        return result;
        };

    // convert pixel values to other units
    var pixelToUnit = function(data, type, px) {
        var ratio = data.measureFactor;
        var result = scaleValue(data, type, px, ratio);
        return result;
        };

    // rectify pixel values according to digilib aspect ratio
    var rectifiedPixel = function(data, shape) {
        var type = shape.geometry.type;
        var display = data.settings.shapeInfo[type].display;
        var px = (display === 'area')
            ? rectifiedArea(data, shape)
            : rectifiedDist(data, shape);
        return px;
        };

    // update last measured pixel values, display as converted to new units
    var updateUnits = function(data) {
        var type = getActiveShapeType(data);
        var px = data.lastMeasuredValue;
        var result = pixelToUnit(data, type, px);
        updateMeasures(data, result, type);
        };

    // display info for shape
    var updateInfo = function(data, shape) {
        data.lastMeasuredValue = rectifiedPixel(data, shape);
        setActiveShapeType(data, shape);
        updateUnits(data);
        };

    // info data for shape
    var getInfoHTML = function(data, shape) {
        var s = data.settings;
        var type = shape.geometry.type;
        var display = s.shapeInfo[type].display;
        var name = s.shapeInfo[type].name;
        var px = (display === 'area')
            ? rectifiedArea(data, shape)
            : rectifiedDist(data, shape);
        var len = fn.cropFloat(pixelToUnit(data, type, px), 2);
        var unit = data.measureWidgets.unit1.find('option:selected').text();
        var html = '<div class="head">'+name+'</div><div><em>'+display+'</em>: '+len+' '+unit+'</div>';
        return html;
        };

    // select/unselect shape (or toggle)
    var selectShape = function(data, elem, shape, select) {
        var css = CSS+'selected';
        if (select == null) { // toggle
            select = !shape.properties.selected }
        var cssclass = shapeClass(shape.geometry.type, select ? css : null);
        $(elem).attr("class", cssclass);
        shape.$elem.attr("class", cssclass);
        shape.properties.cssclass = cssclass;
        shape.properties.selected = select;
    };

    // construct CSS class for svg shape
    var shapeClass = function(shapeType, more) {
        var css = CSS+'shape '+CSS+shapeType;
        if (more != null) { css += ' '+more };
        return css;
        };

    // create a shape of the currently selected shape type
    var newShape = function(data) {
        var shapeType = getActiveShapeType(data);
        var style = data.settings.styles.shape;
        return {
            'id': fn.createId(null, CSS),
            'geometry': {
                'type': shapeType
                },
            'properties': {
                'editable': true,
                'selected': false,
                'stroke': style['stroke'],
                'stroke-width': style['stroke-width'],
                'cssclass': shapeClass(shapeType)
                // 'center' : data.settings.drawFromCenter
                }
            };
        };

    // disable the calibration input 
    var setCalibrationInputState = function(data) {
        var widgets = data.measureWidgets;
        var type = getActiveShapeType(data);
        var display = data.settings.shapeInfo[type].display;
        var state = display !== 'length' && display !== 'radius' && display !== 'spacing';
        widgets.value1.prop('disabled', state);
        widgets.type.text(display);
        };

    // returns a screenpoint manipulation function
    var snapToUnit = function(data) {
        // snap to the next rounded unit distance
        return function(shape) {
            var props = shape.properties;
            var screenpos = props.screenpos;
            var vtx = props.vtx;
            if (screenpos == null || vtx == null) {
                return; }
            var lastPos = screenpos[getLastVertex(shape)];
            var thisPos = screenpos[vtx]; // mouse position
            var fac = data.measureFactor;
            shape.geometry.coordinates[vtx] = data.imgTrafo.invtransform(thisPos);
            var unitDist = getVertexDistance(data, shape) * fac;
            var roundDist = Math.round(unitDist); // round to the nearest integer
            var newPos = (roundDist === 0)
                ? thisPos
                : lastPos.scale(thisPos, roundDist/unitDist); // calculate snap position
            screenpos[vtx].moveTo(newPos);
            };
        };

    // returns a screenpoint manipulation function
    var lockDimension = function(dim) {
        // lock one dimension of the current screen pos to that of the previous
        return function(shape) {
            var props = shape.properties;
            var startpos = props.startpos;
            var screenpos = props.screenpos;
            var vtx = props.vtx;
            if (startpos == null || screenpos == null || vtx == null) {
                return; }
            screenpos[vtx][dim] = startpos[dim];
            };
        };

    // manipulate the screen points of the shape
    var manipulatePosition = function(shape, manipulate) {
        // apply the manipulation function
        manipulate(shape);
        };

    // return the current shape type
    var getActiveShapeType = function(data) {
        return data.settings.activeShapeType;
        };

    // set the current shape type (from shape select widget)
    var changeActiveShapeType = function(data) {
        data.settings.activeShapeType = data.measureWidgets.shape.val();
        setCalibrationInputState(data);
        };

    // set the current shape type
    var setActiveShapeType = function(data, shape) {
        data.settings.activeShapeType = shape.geometry.type;
        setCalibrationInputState(data);
        };

    // update Line Style classes (overwrite CSS)
    var updateLineStyles = function(data) {
        var s = data.settings;
        var DL = s.cssPrefix;
        var $lineStyles = s.$lineStyles;
        var style = s.styles;
        $lineStyles.html(
            '.'+CSS+'guide {stroke: '+style.guide.stroke+'} '+
            '.'+CSS+'constr {stroke: '+style.constr.stroke+'} '+
            '.'+CSS+'selected {stroke: '+style.selected.stroke+'} '+
            'div.'+DL+'digilib .'+DL+'svg-handle {stroke: '+style.handle.stroke+'}'
            );
        var widget = data.measureWidgets;
        var styleName = s.implementedStyles;
        var bg = 'background-color';
        var setColor = function(i, item) {
            widget[item+'color'].css(bg, style[item].stroke)
            };
        $.each(styleName, setColor);
    };

    // load shape types into select element
    var populateShapeSelect = function(data) {
        var $shape = data.measureWidgets.shape;
        var shapeInfo = data.settings.shapeInfo;
        var implementedShape = data.settings.implementedShapes;
        var addOption = function(index, type) {
            $shape.append($('<option value="'+ type + '">' + shapeInfo[type].name + '</option>'));
            };
        $.each(implementedShape, addOption);
        $shape.children()[0].selected = true;
    };

    // load units into select elements
    var populateUnitSelects = function(data) {
        var widgets = data.measureWidgets;
        var $u1 = widgets.unit1;
        var $u2 = widgets.unit2;
        var section = data.settings.units.sections;
        var addOptions = function(i, item) {
            var $opt = $('<option class="dl-section" disabled="disabled">'+ item.name +'</option>');
            $u1.append($opt);
            $u2.append($opt.clone());
            var unit = item.units;
            var addOption = function(i, item) {
				var $opt = $('<option class="dl-units" value="'+ item.factor + '">'+ item.name + '</option>');
				$opt.data('unit', item);
				$u1.append($opt);
				$u2.append($opt.clone());
				};
            $.each(unit, addOption);
            };
        $.each(section, addOptions);
        $u1.children(':not(:disabled)')[data.settings.unitFrom].selected = true;
        $u2.children(':not(:disabled)')[data.settings.unitTo].selected = true;
    };

    // show or hide SVG element (not possible via jQuery .hide/.show)
    var showSVG = function(data, on) {
        var layer = data.measureLayer;
        $svg = layer.$elem;
        if (on) {
            $svg.removeAttr("display"); }
        else {
            $svg.attr("display", "none"); }
    };

    // initial position of measure bar (bottom left of browser window)
    var setScreenPosition = function(data, $bar) {
        if ($bar == null) return;
        var barH = (new geom.Rectangle($bar)).height;
        var screenH = fn.getFullscreenRect(data).height;
        new geom.Position(10, screenH - barH).adjustDiv($bar);
    };

    // drag measureBar around
    var dragMeasureBar = function(event) {
        var $div = $(this).parent();
        var x = $div.offset().left - event.pageX;
        var y = $div.offset().top - event.pageY;
        $(document.body).on('mousemove.measure', function(event) {
            $div.offset({
                left : event.pageX + x,
                top  : event.pageY + y
            });
        }).on('mouseup.measure', function(event) {
            $(document.body).off('mousemove.measure').off('mouseup.measure');
            });
        return false;
        };

    var createInfoDiv = function() {
        var options = { id: CSS+'info', class: 'dl-keep '+CSS+'info' };
        return $('<div>', options);
        };

    // show shape info
    var showInfoDiv = function(event, data, shape) {
        var settings = data.settings;

        var $info = settings.infoDiv;

        $info.fadeIn();

        $info.html(getInfoHTML(data, shape));
        $info.offset({
            left : event.pageX + 4,
            top  : event.pageY + 4
            });
        return false;
        };

    // hide shape info
    var hideInfoDiv = function() {
        var $info = $('#'+CSS+'info');
        $info.fadeOut();
        };

    // remove selected shapes - or the most recent one, if none was selected
    var removeSelectedShapes = function(data) {
        var layer = data.measureLayer;
        var shapes = layer.shapes;
        if (shapes == null) return;
        var shapesDeleted = 0;
        for (var c = shapes.length; c > 0; --c) {
            var index = c-1;
            if (shapes[index].properties.selected) {
                shapesDeleted++;
                shapes.splice(index, 1);
                }
            }
        if (shapesDeleted === 0 && shapes.length > 0) {
            shapes.pop();
            shapesDeleted++;
            };
        layer.renderFn(data, layer);
        console.debug('measure: shapes deleted:', shapesDeleted);
        };

    // keydown event handler (active when measure bar is visible)
    var onKeyDown = function(event, data) {
        var code = event.keyCode;
        var key = event.key;
        // delete selected shapes
        if (code === 46 || key === 'Delete') {
            removeSelectedShapes(data);
            return false;
            }
        // manipulate current vertex position of shape
        if (code === 88 || key === 'x' ||
            code === 89 || key === 'y' ||
            code === 83 || key === 's') {
            if (keystate[key] == null) {
                // fire mousemove event with manipulated coords on keydown
                keystate[key] = event; // save key state
                if (currentShape == null) { return true };
                var props = currentShape.properties;
                var pt = props.screenpos[props.vtx]; // get vertex position
                var eventpos = { pageX: pt.x, pageY: pt.y };
                var evt = jQuery.Event("mousemove.dlVertexDrag", eventpos);
                $(document).trigger(evt);
                }
            return false;
            }
        };

    // keyup event handler (active when measure bar is visible)
    var onKeyUp = function(event, data) {
        var code = event.keyCode;
        var key = event.key;
        delete keystate[key]; // invalidate key state
        return false;
        };

    // attach/detach keyup/down event handlers
    var attachKeyHandlers = function(data, on) {
        if (on) {
            $(document.body).on('keydown.measure',
                function(evt) { onKeyDown(evt, data) });
            $(document.body).on('keyup.measure',
                function(evt) { onKeyUp(evt, data) });
            }
        else {
            $(document.body).off('keydown.measure');
            $(document.body).off('keyup.measure');
            }
        keystate = {};
        };

    // set up additional SVG shapes
    var setupSvgFactory = function(data) {
        var factory = data.shapeFactory;
        if (factory == null) {
            console.error("No SVG factory found: jquery.digilib.vector not loaded?");
            return;
            }
        factory['Proportion'] = {
                'setup' : function (data, shape) {
                    shape.properties.maxvtx = 3;
                },
                'svg' : function (shape) {
                    var $s = factory['LineString'].svg(shape);
                    return $s;
                }
            };
        factory['Intercolumnium'] = {
                'setup' : function (data, shape) {
                    shape.properties.maxvtx = 3;
                },
                'svg' : function (shape) {
                    var props = shape.properties;
                    var guide = CSS+'guide';
                    var $s = factory['LineString'].svg(shape);
                    var place = $s.place;
                    var $c1 = $(fn.svgElement('circle', {'id': shape.id + '-circ1', 'class': guide }));
                    var $c2 = $(fn.svgElement('circle', {'id': shape.id + '-circ2', 'class': guide }));
                    var $g = $(fn.svgElement('g', {'id': shape.id + '-intercolumnium'}));
                    $g.append($s).append($c1).append($c2);
                    $g.place = function () {
                        var p = props.screenpos;
                        var vtx = props.vtx;
                        place.call($s); // place the linestring
                        if (p.length > 2) { // p[2] is the mouse pointer
                            var m1 = p[1].mid(p[2]);
                            var line = new geom.Line(m1, p[1]);
                            var m2 = p[0].copy().add(line.vector());
                            var rad = line.length();
                            $c1.attr({cx: m1.x, cy: m1.y, r: rad});
                            $c2.attr({cx: m2.x, cy: m2.y, r: rad});
                        }
                    }
                    shape.$interactor = $s;
                    return $g;
                }
            };
        factory['Rect'] = {
                'setup' : function (data, shape) {
                    shape.properties.maxvtx = 3;
                },
                'svg' : function (shape) {
                    var trafo = data.imgTrafo;
                    var $s = factory['Polygon'].svg(shape);
                    var props = shape.properties;
                    $s.place = function () {
                        var p = props.screenpos;
                        var vtx = props.vtx;
                        if (p.length > 2) { // p[2] is the mouse pointer
                            var line1 = new geom.Line(p[0], p[1]); // base line
                            var line2 = line1.parallel(p[2]);
                            var p3 = line1.perpendicular().intersection(line2);
                            var p2 = p3.copy().add(line1.vector());
                            p[2] = p2.mid(p3); // handle position
                            shape.geometry.coordinates[2] = trafo.invtransform(p[2]).toArray();
                            props.pos = [p2, p3]; // save other points
                            }
                        this.attr({points: [p[0], p[1], p2, p3].join(" ")});
                        };
                    return $s;
                }
            };
        factory['Oval'] = {
                'setup' : function (data, shape) {
                    shape.properties.maxvtx = 4;
                },
                'svg' : function (shape) {
                    var trafo = data.imgTrafo;
                    var styles = data.settings.styles;
                    var props = shape.properties;
                    props['stroke-width'] = styles.guide['stroke-width']; // draw a rectangle in guides style
                    var $s = factory['Rect'].svg(shape);
                    var place = $s.place;
                    var guide = CSS+'guide';
                    var shapeClass = CSS+'shape';
                    var constr = CSS+'constr';
                    $s.attr({'class' : guide});
                    var $g = $(fn.svgElement('g', {'id': shape.id + '-oval'}));
                    var $c1 = $(fn.svgElement('circle', {'id': shape.id + '-circ1', 'class': guide }));
                    var $c2 = $(fn.svgElement('circle', {'id': shape.id + '-circ2', 'class': guide }));
                    var $p1 = $(fn.svgElement('path',   {'id': shape.id + '-lines', 'class': guide }));
                    var $p2 = $(fn.svgElement('path',   {'id': shape.id + '-constr', 'class': constr })); // debug construction
                    props['stroke-width'] = styles.shape['stroke-width'];
                    var $arc = $(fn.svgElement('path',  fn.svgAttr(data, shape)));
                    $g.append($s).append($c1).append($c2).append($p1).append($p2).append($arc);
                    $g.place = function () {
                        var p = props.screenpos;
                        place.call($s); // place the framing rectangle (polygon)
                        if (p.length > 3) { // p[3] is the mouse pointer
                            var side0 = new geom.Line(p[0], p[1]) // the sides
                            var side1 = new geom.Line(p[1], props.pos[0]);
                            var side2 = new geom.Line(props.pos[0], props.pos[1]);
                            var side3 = new geom.Line(props.pos[1], p[0]);
                            var mid0 = side0.mid(); // the midpoints of the sides
                            var mid1 = side1.mid();
                            var mid2 = side2.mid();
                            var mid3 = side3.mid();
                            var axis1 = side0.parallel(mid3); // short axis
                            var axis2 = side1.parallel(mid0); // long axis
                            var maxDiam = axis1.length()-1; // maximal diameter for small circles
                            var handle = axis2.perpendicularPoint(p[3]); // drag point projected on long axis
                            if (handle.distance(mid0) > axis2.length()) { // constrain handle
                                handle.moveTo(mid2);
                            } else if (handle.distance(mid2) > maxDiam) {
                                handle.moveTo(new geom.Line(mid2, handle).length(maxDiam).point());
                                }
                            var m1 = handle.mid(mid2); // centers of the small circles
                            var m2 = axis1.mirror(m1);
                            var rad1 = m1.distance(mid2); // radius of the small circles
                            var rd1 = axis1.copy().length(rad1).point(); // point in radius distance from midpoint of long axis
                            var rd2 = axis2.mirror(rd1);
                            var md1 = rd1.mid(m1); // midpoint of the line connecting this point with the small circle center
                            var md2 = axis2.mirror(md1);
                            var md3 = axis1.mirror(md1);
                            var md4 = axis1.mirror(md2);
                            var bi = new geom.Line(rd1, m1).perpendicular(md1); // construct the perpendicular bisector of the connection line
                            var m3 = axis1.intersection(bi); // find the centers of the big circles
                            var m4 = axis2.mirror(m3);
                            var fp1 = new geom.Line(m3, m1).addEnd(rad1); // the four fitting points
                            var fp2 = new geom.Line(m3, m2).addEnd(rad1);
                            var fp3 = new geom.Line(m4, m1).addEnd(rad1);
                            var fp4 = new geom.Line(m4, m2).addEnd(rad1);
                            var rad2 = m3.distance(fp1); // radius of the big circles
                            // construct the SVG shapes
                            $c1.attr({cx: m1.x, cy: m1.y, r: rad1});
                            $c2.attr({cx: m2.x, cy: m2.y, r: rad1});
                            $p1.attr({d: // the guidelines
                                'M'+fp1+' L'+m3+' '+fp2+
                                'M'+fp3+' L'+m4+' '+fp4+
                                'M'+rd1+' L'+rd2+
                                'M'+m1+' L'+m2
                            });
                            $p2.attr({d: // the construction lines
                                'M'+mid3+' L'+rd1+' '+m1+' '+mid2+
                                'M'+mid1+' L'+rd2+' '+m2+' '+mid0+
                                'M'+md1+' L'+m3+
                                'M'+md4+' L'+m4
                            });
                            $arc.attr({d: 'M'+fp2+ // the arcs of the oval
                                ' A'+rad2+','+rad2+' 0 0,1 '+fp1+
                                ' A'+rad1+','+rad1+' 0 0,1 '+fp3+
                                ' A'+rad2+','+rad2+' 0 0,1 '+fp4+
                                ' A'+rad1+','+rad1+' 0 0,1 '+fp2
                            });
                            p[3] = handle;
                            shape.geometry.coordinates[3] = trafo.invtransform(handle).toArray();
                            props.measures = { rad1: rad1, rad2: rad2, axis1: axis1.length(), axis2: axis2.length() }; // use for info
                            }
                        };
                    shape.$interactor = $arc;
                    return $g;
                }
            };
        factory['Grid'] = {
                'setup' : function (data, shape) {
                    shape.properties.maxvtx = 2;
                },
                'svg' : function (shape) {
                    var $s = factory['Line'].svg(shape);
                    var place = $s.place;
                    var gridID = shape.id + '-grid';
                    var props = shape.properties;
                    var $g = $(fn.svgElement('g', {id: shape.id + '-g'}));
                    var $defs = $(fn.svgElement('defs'));
                    var $pat = $(fn.svgElement('pattern', {id: gridID, height: '10%', width: '10%', patternUnits: 'objectBoundingBox'}));
                    var $path = $(fn.svgElement('path', {d: "M1000,0 L0,0 0,1000", fill: 'none', stroke: props.stroke, 'stroke-width': '1'}));
                    var $r = $(fn.svgElement('rect', {id: shape.id + '-rect', stroke: props.stroke, fill: 'url(#'+gridID+')'}));
                    $g.append($defs.append($pat.append($path))).append($r).append($s);
                    $g.place = function () {
                        place.call($s);
                        var p = props.screenpos;
                        var d = p[0].distance(p[1]);
                        var angle = fn.cropFloat(p[0].deg(p[1]));
                        var scale = 10;
                        var fac = Math.ceil((1-scale)/2);
                        var x = p[0].x + fac * d;
                        var y = p[0].y + (fac-1) * d;
                        var transform = 'rotate('+angle+' '+p[0].x+' '+p[0].y+')';
                        $r.attr({x:x, y:y, height:d*scale, width:d*scale, transform:transform});
                        $pat.attr({patternTransform:transform});
                        };
                    shape.$interactor = $s;
                    return $g;
                }
            };
        };

    // add a style element to head, for changing line class styles
    var setupLineStyles = function(data) {
        var $head = $('head');
        var $lineStyles = $('<style></style>');
        $head.append($lineStyles);
        data.settings.$lineStyles = $lineStyles;
        updateLineStyles(data);
        var widget = data.measureWidgets;
        if ($.fn.colorPicker == null) {
            return; }
        var styleName = data.settings.implementedStyles;
        var style = data.settings.styles;
        var setupColorPicker = function(i, item) {
            var changeStroke = function(color) {
                style[item].stroke = color;
                updateLineStyles(data);
                };
            var w = widget[item+'color'];
            w.colorPicker({
                pickerDefault : style[item].stroke,
                onColorChange : changeStroke
                });
            };
        $.each(styleName, setupColorPicker);
        };

    var setupMeasureBar = function(data) {
        console.debug('measure: setupMeasureBar');
        var widgets = {
            names : [
                'info',
                'startb', 'shape',
                'type',
                'value1', 'unit1', 'eq',
                'value2', 'unit2',
                'shapecolor', 'guidecolor', 'constrcolor', 'selectedcolor', 'handlecolor',
                'move'
                ],
            info :       $('<img id="dl-measure-info" src="img/info.png" title="display info window for shapes"></img>'),
            startb :     $('<button id="dl-measure-startb" title="click to draw a measuring shape on top of the image">M</button>'),
            shape :      $('<select id="dl-measure-shape" title="select a shape to use for measuring" />'),
			eq :         $('<span class="dl-measure-label">=</span>'),
			type :       $('<span id="dl-measure-shapetype" class="dl-measure-label">length</span>'),
			fac :        $('<span id="dl-measure-factor" class="dl-measure-number" />'),
			value1 :     $('<input id="dl-measure-value1" class="dl-measure-input" title="last measured value - click to change the value" value="0.0" />'),
			value2 :     $('<span id="dl-measure-value2" class="dl-measure-label" title="last measured value, converted to the secondary unit" value="0.0"/>'),
			unit1 :      $('<select id="dl-measure-unit1" title="current measuring unit - click to change" />'),
			unit2 :      $('<select id="dl-measure-unit2" title="secondary measuring unit - click to change" />'),
			angle :      $('<span id="dl-measure-angle" class="dl-measure-number" title="last measured angle" />'),
            shapecolor : $('<span id="dl-measure-shapecolor" class="dl-measure-color" title="select line color for shapes"></span>'),
            guidecolor : $('<span id="dl-measure-guidecolor" class="dl-measure-color" title="select guide line color for shapes"></span>'),
            constrcolor :$('<span id="dl-measure-constrcolor" class="dl-measure-color" title="select construction line color for shapes"></span>'),
            selectedcolor :$('<span id="dl-measure-selectedcolor" class="dl-measure-color" title="select line color for selected shapes"></span>'),
            handlecolor :$('<span id="dl-measure-handlecolor" class="dl-measure-color" title="select color for shape handles"></span>'),
            move :       $('<img id="dl-measure-move" src="img/move.png" title="move measuring bar around the screen"></img>')
		    };
        var $measureBar = $('<div id="dl-measure-toolbar" />');
        var widgetName = widgets.names;
        var appendWidget = function (i, item) {
            $measureBar.append(widgets[item]);
            };
        $.each(widgetName, appendWidget);
        data.$elem.append($measureBar);
        data.$measureBar = $measureBar;
        widgets.fac.text(fn.cropFloatStr(data.measureFactor));
        data.measureWidgets = widgets;
        populateShapeSelect(data);
        populateUnitSelects(data);
        setupMeasureWidgets(data);
        setupLineStyles(data);
        setScreenPosition(data, $measureBar);
        widgets.move.on('mousedown.measure', dragMeasureBar);
        return $measureBar;
        };

    // wire the draw button and widgets
    var setupMeasureWidgets = function (data) {
        console.debug('measure: setupMeasureWidgets');
        var widgets = data.measureWidgets;
        var $startb = widgets.startb;
        var buttonConfig = buttons['drawshape']; // not in data.settings.buttons
        // button properties
        var action = buttonConfig.onclick;
        var tooltip = buttonConfig.tooltip;
        $startb.attr('title', tooltip);
        $elem = data.$elem;
        $startb.on('mousedown.measure', function(evt) {
            // prevent mousedown event ot bubble up to measureBar (no dragging!)
            console.debug('measure: startb mousedown=', action, ' evt=', evt);
            $elem.digilib(action);
            return false;
            });
        widgets.shape.on('change.measure',  function(evt) { changeActiveShapeType(data) });
        widgets.value1.on('change.measure', function(evt) { changeFactor(data) });
        widgets.unit1.on('change.measure',  function(evt) { updateUnits(data) });
        widgets.unit2.on('change.measure',  function(evt) { updateUnits(data) });
        widgets.unit1.attr('tabindex', -1);
        widgets.unit2.attr('tabindex', -1);
        widgets.value1.attr('tabindex', -1);
        };

    // event handler for setup phase
    var handleSetup = function (evt) {
        console.debug("measure: handleSetup");
        var data = this;
        data.lastMeasuredValue = 0;
        data.lastMeasuredAngle = 0;
        data.measureFactor = 1.0,
        setupMeasureBar(data);
        setupSvgFactory(data);
        data.measureLayer = {
            'shapes': [],
            'projection': 'screen',
            'handleType': 'diamond'
            };
        digilib.actions.addVectorLayer(data, data.measureLayer);
        };

    // event handler for scaler update
    var handleUpdate = function (evt) {
        var data = this;
        console.debug("measure: handleUpdate");
        };

    // plugin installation called by digilib on plugin object
    var install = function (plugin) {
        digilib = plugin;
        if (digilib.plugins.vector == null) {
            console.error('measure: jquery.digilib.vector.js is missing, aborting installation.');
            return;
            }
        console.debug('installing measure plugin. digilib:', digilib);
        fn = digilib.fn;
        // import geometry classes
        geom = fn.geometry;
        // add defaults, actions, buttons
        $.extend(true, digilib.defaults, defaults);
        $.extend(digilib.actions, actions);
        $.extend(true, digilib.buttons, buttons);
        // insert in button list -- not elegant
        if (digilib.plugins.buttons != null) {
            // if (digilib.defaults.buttonSettings != null) {
            digilib.defaults.buttonSettings.fullscreen.standardSet.splice(10, 0, 'measure');
            }
        // export functions
        // fn.test = test;
        };

    // plugin initialization
    var init = function (data) {
        console.debug('initialising measure plugin. data:', data);
        var settings = data.settings;
        CSS = settings.cssPrefix+'measure-';
        FULL_AREA  = new geom.Rectangle(0, 0, 1, 1);
        // install event handlers
        var $data = $(data);
        $data.on('setup', handleSetup);
        $data.on('update', handleUpdate);
        $data.on('createShape', onCreateShape);
        $data.on('renderShape', onRenderShape);
        $data.on('changeShape', onChangeShape);
        $data.on('positionShape', onPositionShape);
        $data.on('dragShape', onDragShape);
        var $info = createInfoDiv();
        var timer;
        settings.infoDiv = $info;
        $info.appendTo(data.$elem)
            .on('mouseout.measureinfo', function() { timer = setTimeout(hideInfoDiv, 300) })
            .on('mouseover.measureinfo', function() { clearTimeout(timer) });
        };

    // plugin object with name and init
    // shared objects filled by digilib on registration
    var pluginProperties = {
            name : 'measure',
            install : install,
            init : init,
            buttons : {},
            actions : {},
            fn : {},
            plugins : {}
        };

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