changeset 1488:49a5b1e73098

new digicat works now. has rows and cols as parameters. no ui for setting geometry yet. does not use sequence plugin yet.
author robcast
date Fri, 26 Feb 2016 18:57:43 +0100
parents 684632a23c80
children 02e055d9b84e
files webapp/src/main/webapp/jquery/digicat.html webapp/src/main/webapp/jquery/jquery.digicat.buttons.js webapp/src/main/webapp/jquery/jquery.digicat.css webapp/src/main/webapp/jquery/jquery.digicat.geometry.js webapp/src/main/webapp/jquery/jquery.digicat.js webapp/src/main/webapp/jquery/jquery.digilib.geometry.js
diffstat 6 files changed, 1651 insertions(+), 40 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/webapp/src/main/webapp/jquery/digicat.html	Fri Feb 26 18:57:43 2016 +0100
@@ -0,0 +1,40 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+    <head>
+        <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+        <meta name="viewport" content="initial-scale=1.0"/>
+        <title>digicat TEST</title>
+
+        <style type="text/css">
+            body {
+                 background: silver;
+            }
+        </style>
+
+        <script type="text/javascript" src="jquery.js"></script>
+        <script type="text/javascript" src="jquery.cookie.js"></script>
+        <script type="text/javascript" src="jquery.digicat.js"></script>
+        <script type="text/javascript" src="jquery.digicat.geometry.js"></script>
+        <script type="text/javascript" src="jquery.digicat.buttons.js"></script>
+        <link rel="stylesheet" type="text/css" href="jquery.digicat.css" />
+        <link rel="stylesheet" type="text/css" href="jquery.digilib.buttons-full-32-sprite.css" />
+
+        <script type="text/javascript">
+            $(document).ready(function(){
+                var opts = {
+                    };
+                var $div = $('div#digicat');
+                $div.digicat(opts);
+            });
+
+        </script>
+    </head>
+
+    <body>
+        <div id="digicat">
+            <p>digicat doesn't work! Please switch on Javascript or notify the server administrator!</p>
+            <img src="http://digilib.sourceforge.net/images/digilib-logo-big.png" />
+        </div>
+    </body>
+</html>
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/webapp/src/main/webapp/jquery/jquery.digicat.buttons.js	Fri Feb 26 18:57:43 2016 +0100
@@ -0,0 +1,510 @@
+/*
+ * #%L
+ * digilib buttons plugin
+ * %%
+ * Copyright (C) 2011 - 2013 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
+ */
+/**
+digilib buttons plugin
+ */
+
+(function($) {
+
+    // plugin object with digilib data
+    var digilib = null;
+    // the functions made available by digilib
+    var fn = null;
+    // affine geometry plugin
+    var geom = null;
+
+    var buttons = {
+        reference : {
+            onclick : "reference",
+            tooltip : "get a reference URL",
+            icon : "reference.png"
+            },
+        zoomin : {
+            onclick : ["zoomBy", 1.4],
+            tooltip : "zoom in",
+            icon : "zoom-in.png"
+            },
+        zoomout : {
+            onclick : ["zoomBy", 0.7],
+            tooltip : "zoom out",
+            icon : "zoom-out.png"
+            },
+        zoomarea : {
+            onclick : "zoomArea",
+            tooltip : "zoom area",
+            icon : "zoom-area.png"
+            },
+        zoomfull : {
+            onclick : "zoomFull",
+            tooltip : "view the whole image",
+            icon : "zoom-full.png"
+            },
+        pagewidth : {
+            onclick : ["zoomFull", "width"],
+            tooltip : "page width",
+            icon : "pagewidth.png"
+            },
+        back : {
+            onclick : ["gotoPage", "-1"],
+            tooltip : "goto previous page",
+            icon : "back.png"
+            },
+        fwd : {
+            onclick : ["gotoPage", "+1"],
+            tooltip : "goto next page",
+            icon : "fwd.png"
+            },
+        page : {
+            onclick : "gotoPage",
+            tooltip : "goto page number",
+            icon : "page.png"
+            },
+        about : {
+            onclick : "about",
+            tooltip : "about Digilib",
+            icon : "info.png"
+            },
+        reset : {
+            onclick : "reset",
+            tooltip : "reset image",
+            icon : "reset.png"
+            },
+        hmir : {
+            onclick : ["mirror", "h"],
+            tooltip : "mirror horizontally",
+            icon : "mirror-horizontal.png"
+            },
+        vmir : {
+            onclick : ["mirror", "v"],
+            tooltip : "mirror vertically",
+            icon : "mirror-vertical.png"
+            },
+        rot : {
+            onclick : "rotate",
+            tooltip : "rotate image",
+            icon : "rotate.png"
+            },
+        brgt : {
+            onclick : "brightness",
+            tooltip : "set brightness",
+            icon : "brightness.png"
+            },
+        cont : {
+            onclick : "contrast",
+            tooltip : "set contrast",
+            icon : "contrast.png"
+            },
+        rgb : {
+            onclick : "setRGB",
+            tooltip : "set rgb values",
+            icon : "rgb.png"
+            },
+        quality : {
+            onclick : "setQuality",
+            tooltip : "set image quality",
+            icon : "quality.png"
+            },
+        size : {
+            onclick : "setSize",
+            tooltip : "set page size",
+            icon : "size-bigger.png"
+            },
+        calibrationx : {
+            // onclick : "showCalibrationDiv",
+            onclick : "calibrate",
+            tooltip : "calibrate screen resolution",
+            icon : "calibration.png"
+            },
+        scale : {
+            // onclick : "showScaleModeSelector",
+            onclick : "setScaleMode",
+            tooltip : "change image scale",
+            icon : "original-size.png"
+            },
+        toggleoptions : {
+            onclick : "moreButtons",
+            tooltip : "more options",
+            icon : "options.png"
+            },
+        moreoptions : {
+            onclick : ["moreButtons", "+1"],
+            tooltip : "more options",
+            icon : "buttons-more.png"
+            },
+        lessoptions : {
+            onclick : ["moreButtons", "-1"],
+            tooltip : "less options",
+            icon : "buttons-less.png"
+            },
+        SEP : {
+            icon : "sep.png"
+            }
+        };
+
+    var modes = [
+        {   name : "screen", 
+            label : "fit to screen",
+            tooltip : "scales the graphic file so that it fills the screen"
+        },
+        {   name : "pixel",
+            label : "pixel by pixel",
+            tooltip : "all pixels of the current part of the graphic file are shown"
+        },
+        {   name : "size",
+            label : "original size",
+            tooltip : "tries to display the current part of the graphic file in the size of the orginal resource (after screen calibration)" 
+        }
+    ];
+
+    var defaults = {
+        // buttons (reference added later)
+        'buttons' : null,
+        // disabled buttons (should be an array of button names)
+        'buttonsDisabled' : [],
+        // show buttons needed for consecutive (book-like) consultation of image files
+        'showPageButtons' : true,
+        // defaults for digilib buttons
+        'buttonSettings' : {
+            'fullscreen' : {
+                // path to button images (must end with a slash)
+                'imagePath' : 'img/fullscreen/32/',
+                'buttonSetWidth' : 36,
+                'standardSet' : ["back","fwd","page","about"],
+                'specialSet' : ["mark","delmark","hmir","vmir","rot","brgt","cont","rgb","quality","size","calibrationx","scale","lessoptions","moreoptions"],
+                'pageSet' : ["back","fwd","page"],
+                'buttonSets' : ['standardSet', 'specialSet']
+                },
+            'embedded' : {
+                'imagePath' : 'img/embedded/16/',
+                'buttonSetWidth' : 18,
+                'standardSet' : ["reference","zoomin","zoomout","zoomarea","zoomfull","about","reset","moreoptions"],
+                'specialSet' : ["mark","delmark","hmir","vmir","rot","brgt","cont","rgb","quality","scale","lessoptions"],
+                'pageSet' : ["back","fwd","page"],
+                'buttonSets' : ['standardSet', 'specialSet']
+                }
+        },
+        // number of visible button groups
+        'visibleButtonSets' : 1
+    };
+
+    var actions = {
+            // display more (or less) button sets
+            moreButtons : function (data, more) {
+                var settings = data.settings;
+                if (more == null) {
+                    // toggle more or less (only works for 2 sets)
+                    var maxbtns = settings.buttonSettings[settings.interactionMode].buttonSets.length;
+                    if (settings.visibleButtonSets >= maxbtns) {
+                        more = '-1';
+                    } else {
+                        more = '+1';
+                    }
+                }
+                if (more === '-1') {
+                    // remove set
+                    var setIdx = settings.visibleButtonSets - 1;
+                    if (showButtons(data, false, setIdx, true)) {
+                        settings.visibleButtonSets--;
+                    }
+                } else {
+                    // add set
+                    var setIdx = settings.visibleButtonSets;
+                    if (showButtons(data, true, setIdx, true)) {
+                        settings.visibleButtonSets++;
+                    }
+                }
+                // adjust insets
+                data.currentInsets['buttons'] = getInsets(data);
+                // persist setting
+                fn.storeOptions(data);
+            }
+    };
+
+    // plugin installation called by digilib on plugin object.
+    var install = function(plugin) {
+        digilib = plugin;
+        console.debug('installing buttons plugin. digilib:', digilib);
+        fn = digilib.fn;
+        // import geometry classes
+        geom = fn.geometry;
+        // add defaults, actions, buttons
+        $.extend(digilib.buttons, buttons);
+        $.extend(true, digilib.defaults, defaults); // make deep copy
+        $.extend(digilib.actions, actions);
+        // update buttons reference in defaults
+        digilib.defaults.buttons = digilib.buttons;
+        // export functions
+        fn.createButton = createButton;
+        fn.highlightButtons = highlightButtons;
+        fn.setButtonAction = setButtonAction;
+        fn.findButtonByName = findButtonByName;
+    };
+
+    // plugin initialization
+    var init = function (data) {
+        console.debug('initialising buttons plugin. data:', data);
+        var settings = data.settings;
+        // add insets
+        data.currentInsets['buttons'] = getInsets(data);
+        if (!settings.showPageButtons) {
+            var pageSet = settings.buttonSettings[settings.interactionMode].pageSet;
+            $.merge(settings.buttonsDisabled, pageSet);
+        }
+        // install event handler
+        var $data = $(data);
+        $data.bind('setup', handleSetup);
+    };
+
+    var handleSetup = function (evt) {
+        console.debug("buttons: handleSetup");
+        var data = this;
+        var settings = data.settings;
+        // create buttons before scaler 
+        for (var i = 0; i < settings.visibleButtonSets; ++i) {
+            showButtons(data, true, i);
+        }
+        disableButtons(data);
+    };
+
+    /** 
+     * returns insets for buttons (based on visibleButtonSets and buttonSetWidth
+     */
+    var getInsets = function (data) {
+        var settings = data.settings;
+        var bw = settings.visibleButtonSets * settings.buttonSettings[settings.interactionMode].buttonSetWidth;
+        var insets = {'x' : bw, 'y' : 0};
+        return insets;
+    };
+
+    /**
+     *  creates HTML structure for a single button
+     */
+    var createButton = function (data, $div, buttonName) {
+        var $elem = data.$elem;
+        var settings = data.settings;
+        var cssPrefix = settings.cssPrefix;
+        var mode = settings.interactionMode;
+        var imagePath = settings.buttonSettings[mode].imagePath;
+        // make relative imagePath absolute
+        if (imagePath.charAt(0) !== '/' && imagePath.substring(0,7) !== 'http://') {
+        	imagePath = settings.digilibBaseUrl + '/jquery/' + imagePath;
+        }
+        var buttonConfig = settings.buttons[buttonName];
+        if (buttonConfig == null) {
+            console.error('Could not create button: ' + buttonName);
+            return;
+            }
+        // button properties
+        var action = buttonConfig.onclick;
+        var tooltip = buttonConfig.tooltip;
+        var iconId = buttonConfig.icon.slice(0, -4);
+        // construct the button html
+        var html = '\
+            <div id="'+cssPrefix+'button-'+buttonName+'" class="'+cssPrefix+'button" title="'+tooltip+'">\
+                <a href="">\
+                    <div id="'+cssPrefix+'button-'+iconId+'-img"></div>\
+                </a>\
+            </div>';
+        var $button = $(html);
+        $button.appendTo($div);
+        // create handler for the buttons
+        $button.on('click.digilib', (function () {
+            // we create a new closure to capture the value of action
+            if ($.isArray(action)) {
+                // the handler function calls digilib with action and parameters
+                return function (evt) {
+                    console.debug('click action=', action, ' evt=', evt);
+                    $elem.digicat.apply($elem, action);
+                    return false;
+                };
+            } else {
+                // the handler function calls digilib with action
+                return function (evt) {
+                    console.debug('click action=', action, ' evt=', evt);
+                    $elem.digicat(action);
+                    return false;
+                };
+            }
+        })());
+        return $button;
+    };
+
+    // creates HTML structure for buttons in elem
+    var createButtons = function (data, buttonSetIdx) {
+        var $elem = data.$elem;
+        var settings = data.settings;
+        var mode = settings.interactionMode;
+        var cssPrefix = settings.cssPrefix;
+        var buttonSettings = settings.buttonSettings[mode];
+        var buttonGroup = buttonSettings.buttonSets[buttonSetIdx];
+        if (buttonGroup == null) {
+            // no buttons here
+            return;
+        }
+        // button divs are marked with class "keep"
+        var $buttonsDiv = $('<div class="'+cssPrefix+'keep '+cssPrefix+'buttons"/>');
+        var buttonNames = buttonSettings[buttonGroup];
+        for (var i = 0; i < buttonNames.length; i++) {
+            var buttonName = buttonNames[i];
+            var $button = createButton(data, $buttonsDiv, buttonName);
+            settings.buttons[buttonName].button = $button;
+        }
+        // make buttons div scroll if too large for window
+        if ($buttonsDiv.height() > $(window).height() - 10) {
+            $buttonsDiv.css('position', 'absolute');
+        }
+        // buttons hidden at first
+        $buttonsDiv.hide();
+        $elem.append($buttonsDiv);
+        if (data.$buttonSets == null) {
+            // first button set
+            data.$buttonSets = [$buttonsDiv];
+        } else {
+            $elem.append($buttonsDiv);
+            data.$buttonSets[buttonSetIdx] = $buttonsDiv;
+        }
+        return $buttonsDiv;
+    };
+
+    // display more (or less) button sets
+    var showButtons = function (data, more, setIdx, animated) {
+        var atime = animated ? 'fast': 0;
+        var cssPrefix = data.settings.cssPrefix;
+        // get button width from settings
+        var mode = data.settings.interactionMode;
+        var btnWidth = data.settings.buttonSettings[mode].buttonSetWidth;
+        if (more) {
+            // add set
+            var $otherSets = data.$elem.find('div.'+cssPrefix+'buttons:visible');
+            var $set;
+            if (data.$buttonSets && data.$buttonSets[setIdx]) {
+                // set exists
+                $set = data.$buttonSets[setIdx];
+            } else {
+                $set = createButtons(data, setIdx);
+                }
+            if ($set == null) return false;
+            // include border in calculation
+            //var btnWidth = $set.outerWidth();
+            // console.debug("btnWidth", btnWidth);
+            // move remaining sets left and show new set
+            if ($otherSets.length > 0) {
+                    $otherSets.animate({right : '+='+btnWidth+'px'}, atime,
+                            function () {$set.show();});
+            } else {
+                $set.show();
+            }
+        } else {
+            // remove set
+            var $set = data.$buttonSets[setIdx];
+            if ($set == null) return false;
+            //var btnWidth = $set.outerWidth();
+            // hide last set
+            $set.hide();
+            // take remaining sets and move right
+            var $otherSets = data.$elem.find('div.'+cssPrefix+'buttons:visible');
+            $otherSets.animate({right : '-='+btnWidth+'px'}, atime);
+        }
+        return true;
+    };
+
+    // check for buttons to highlight TODO: improve this!
+    var highlightButtons = function (data, name, on) {
+        var cssPrefix = data.settings.cssPrefix;
+        var $buttons = data.$elem.find('div.'+cssPrefix+'buttons:visible'); // include hidden?
+        // add a class for highlighted button
+        var highlight = function (name, on) {
+            var $button = findButtonByName(data, name);
+            if (on) {
+                $button.addClass(cssPrefix+'button-on');
+            } else {
+                $button.removeClass(cssPrefix+'button-on');
+            }
+        };
+        if (name != null) {
+            return highlight(name, on);
+            }
+        var flags = data.scalerFlags;
+        var settings = data.settings;
+        highlight('rot', settings.rot);
+        highlight('brgt', settings.brgt);
+        highlight('cont', settings.cont);
+        highlight('bird', settings.isBirdDivVisible);
+        highlight('hmir', flags.hmir);
+        highlight('vmir', flags.vmir);
+        highlight('quality', flags.q1 || flags.q2);
+        highlight('zoomin', ! isFullArea(data.zoomArea));
+        };
+
+    // find a button
+    var findButtonByName = function (data, name) {
+        var $elem = data.$elem;
+        var cssPrefix = data.settings.cssPrefix;
+        var $button = $elem.find('#'+cssPrefix+'button-'+name);
+        console.debug("find button", name, $button);
+        return $button;
+    };
+
+    // hide disabled buttons
+    var disableButtons = function (data, buttonnames) {
+        // if present, buttonnames should be an array of button names
+        var $elem = data.$elem;
+        var settings = data.settings;
+        var cssPrefix = settings.cssPrefix;
+        var disabled = buttonnames || settings.buttonsDisabled;
+        $.each(disabled, function(index, name) {
+            var $button = findButtonByName(data, name);
+            $button.addClass(cssPrefix+'disabled');
+            });
+        console.debug('disabled buttons:', disabled);
+    };
+
+    // set standard button "onclick" field to a new action
+    var setButtonAction = function(buttonName, action) {
+        var button = buttons[buttonName];
+        if (button == null) {
+            console.log('could not set button action ' + action 
+                + ', button ' + buttonName + ' not available' );
+            return;
+            }
+        button.onclick = action;
+    };
+
+    // plugin object with name and init
+    // shared objects filled by digilib on registration
+    var plugin = {
+            name : 'buttons',
+            install : install,
+            init : init,
+            buttons : {},
+            actions : {},
+            fn : {},
+            plugins : {}
+    };
+
+    if ($.fn.digicat == null) {
+        $.error("jquery.digicat.buttons must be loaded after jquery.digicat!");
+    } else {
+        $.fn.digicat('plugin', plugin);
+    }
+})(jQuery);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/webapp/src/main/webapp/jquery/jquery.digicat.css	Fri Feb 26 18:57:43 2016 +0100
@@ -0,0 +1,73 @@
+/* 
+ * CSS style file for jQuery digilib
+ *
+ * Martin Raspe, Robert Casties, 11.1.2011
+ */
+* {
+    -moz-box-sizing: border-box;
+    -webkit-box-sizing: border-box;
+    box-sizing: border-box;
+}
+
+div.dl-digicat .dl-disabled {
+    display: none;
+}
+
+div.dl-digicat,
+div.dl-digicat button,
+div.dl-digicat input {
+   font-family: Verdana, Arial, Helvetica, sans-serif;
+   font-size: 12px;
+}
+
+    
+div.dl-digicat div.dl-about {
+	position: absolute;
+	padding: 10px;	
+	font-family: Verdana, Arial, Helvetica, sans-serif;
+	border: 2px solid aqua;
+	background-color: silver;
+	text-align: center;
+	display: none;
+	z-index: 100;
+}
+
+div.dl-digicat .dl-error {
+	color: red;
+	background-color: maroon;
+	}
+
+div.dl-digicat div.dl-buttons {
+	position: fixed;
+	right: 0px;
+	top: 0px;
+	padding: 2px;
+	background-color: transparent;
+	z-index: 200;
+}
+
+div.dl-digicat div.dl-buttons div.dl-button {
+	margin-bottom: 3px;
+}
+
+div.dl-digicat div.dl-buttons div.dl-button:hover {
+	background-image: url('img/fullscreen/corona.png');
+	background-repeat: no-repeat;
+}
+
+div.dl-digicat div.dl-button-on {
+	background-image: url('img/fullscreen/whitedisc.png');
+	background-repeat: no-repeat;
+}
+
+/* no borders for button images */
+div.dl-digicat a img.dl-button {
+    border:none;
+}
+
+/* table contents */
+div.dl-digicat td {
+	text-align: center;
+}
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/webapp/src/main/webapp/jquery/jquery.digicat.geometry.js	Fri Feb 26 18:57:43 2016 +0100
@@ -0,0 +1,811 @@
+/*
+ * #%L
+ * required digilib geometry plugin
+ * %%
+ * Copyright (C) 2011 - 2013 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
+ */
+/** required digilib geometry plugin
+ */
+
+(function($) {
+//var dlGeometry = function() {
+    /*
+     * Size class
+     */
+    var size = function(w, h) {
+        var that;
+        if (typeof w === "object") {
+            // assume an object having width and height
+            that = {
+                width : w.width,
+                height : w.height
+            };
+        } else {
+            that = {
+                width : parseFloat(w),
+                height : parseFloat(h)
+            };
+        }
+        // returns true if both sizes are equal
+        that.equals = function(other) {
+            return (this.width === other.width && this.height === other.height);
+        };
+        // returns the aspect ratio of this size
+        that.getAspect = function() {
+            return (this.width / this.height);
+        };
+        // returns a size of a given aspect ratio that fits into this one 
+        that.fitAspect = function(aspect) {
+            var s = size(this);
+            if (aspect > this.getAspect()) {
+                // size is more horizontally stretched than this
+                s.height = s.width / aspect;
+            } else {
+                s.width = s.height * aspect;
+            }
+            return s;
+        };
+        // adjusts size of jQuery element "$elem" to this size
+        that.adjustDiv = function($elem) {
+            $elem.width(this.width).height(this.height);
+        };
+        that.toString = function() {
+            return (this.width + "x" + this.height);
+        };
+        return that;
+    };
+
+    /*
+     * Position class
+     */
+    var position = function(x, y) {
+        var that;
+        if (typeof x === "object") {
+            if (x instanceof jQuery) {
+                // jQuery object
+                var pos = x.offset();
+                that = {
+                    x : pos.left,
+                    y : pos.top
+                };
+            } else if ($.isArray(x)) {
+                that = {
+                    x : x[0],
+                    y : x[1]
+                };
+            } else {
+                if (x.x != null) {
+                    // position object
+                    that = {
+                        x : parseFloat(x.x),
+                        y : parseFloat(x.y)
+                    };
+                }
+                if (x.pageX != null) {
+                    // event object
+                    that = {
+                        x : x.pageX,
+                        y : x.pageY
+                    };
+                }
+            }
+        } else {
+            that = {
+                x : parseFloat(x),
+                y : parseFloat(y)
+            };
+        };
+        // return a copy of this position
+        that.copy = function() {
+            return position(this);
+        };
+        // compare function
+        that.equals = function(other) {
+            return (this.x === other.x && this.y === other.y);
+        };
+        // add vector or position to this
+        that.add = function(other) {
+            if ($.isArray(other)) {
+                this.x += other[0];
+                this.y += other[1];
+            } else {
+                this.x += other.x;
+                this.y += other.y;
+            }
+            return this;
+        };
+        // returns negative position
+        that.neg = function() {
+            return position({
+                x : -this.x,
+                y : -this.y
+            });
+        };
+        // returns new position that is the difference between this and other
+        that.delta = function(other) {
+            return position({
+                x : other.x - this.x,
+                y : other.y - this.y
+            });
+        };
+        // returns other position scaled by ratio with regard to this point
+        that.scale = function(other, ratio) {
+            var d = this.delta(other);
+            return position({
+                x : this.x + d.x * ratio,
+                y : this.y + d.y * ratio
+            });
+        };
+        // adjusts CSS position of $elem to this position
+        that.adjustDiv = function($elem) {
+            $elem.offset({
+                left : this.x,
+                top : this.y
+            });
+        };
+        // move this position to another
+        that.moveTo = function(other) {
+            this.x = other.x;
+            this.y = other.y;
+            return this;
+        };
+        // adjust this position so that is is inside rect
+        that.clipTo = function (rect) {
+            var p1 = rect.getPt1();
+            var p2 = rect.getPt2();
+            this.x = Math.max(this.x, p1.x);
+            this.y = Math.max(this.y, p1.y);
+            this.x = Math.min(this.x, p2.x);
+            this.y = Math.min(this.y, p2.y);
+            return this;
+        };
+        // returns distance of this position to pos (length if pos == null)
+        that.distance = function(pos) {
+            if (pos == null) {
+                pos = {
+                    x : 0,
+                    y : 0
+                };
+            }
+            var dx = pos.x - this.x;
+            var dy = pos.y - this.y;
+            return Math.sqrt(dx * dx + dy * dy);
+        };
+        // nearest of several points
+        that.nearest = function (points) {
+            var nearest = points[0];
+            var dist = this.distance(nearest);
+            $.each(points, function(index, item) {
+                var len = this.distance(item);
+                if (len < dist) {
+                    dist = len;
+                    nearest = item;
+                    }
+                });
+            return nearest;
+        };
+        // midpoint of this and other pos
+        that.mid = function (pos) {
+            return position({
+                x : (this.x + pos.x)/2,
+                y : (this.y + pos.y)/2
+            });
+        };
+        // radians of angle between line and the positive X axis
+        that.rad = function (pos) {
+            return Math.atan2(pos.y - this.y, pos.x - this.x);
+        };
+
+        // degree of angle between line and the positive X axis
+        that.deg = function (pos) {
+            return this.rad(pos) / Math.PI * 180;
+        };
+
+        // returns position in css-compatible format
+        that.getAsCss = function() {
+            return {
+                left : this.x,
+                top : this.y
+            };
+        };
+        // return as string
+        that.toString = function() {
+            return (this.x + "," + this.y);
+        };
+        // return as array
+        that.toArray = function() {
+            return [this.x, this.y];
+        };
+        return that;
+    };
+
+    /*
+     * Line class (for on-screen geometry)
+     */
+    var line = function(p, q) {
+        var that = { // definition point
+            x : p.x,
+            y : p.y
+            };
+        if (q.x != null) { // second point
+            that.dx = q.x - that.x;
+            that.dy = q.y - that.y;
+        } else if ($.isArray(q)) { // vector
+            that.dx = q[0];
+            that.dy = q[1];
+        } else if (q === 0) { // slope
+            that.dx = 0;
+            that.dy = 1;
+        } else if (q === Infinity) {
+            that.dx = 1;
+            that.dy = 0;
+        } else if (q === -Infinity) {
+            that.dx = -1;
+            that.dy = 0;
+        } else if (typeof q === 'number' && isFinite(q)) {
+            that.dx = 1;
+            that.dy = 1/q;
+        } else {
+            that.dx = 1;
+            that.dy = 1;
+            }
+        // get/set origin of line
+        that.origin = function(p) {
+            if (p == null) {
+                return position(this.x, this.y);
+                }
+            this.x = p.x;
+            this.y = p.y;
+            return this;
+            };
+        // get/set vector
+        that.vector = function(vector) {
+            if (vector == null) {
+                return [this.dx, this.dy];
+                }
+            this.dx = vector[0];
+            this.dy = vector[1];
+            return this;
+            };
+        // return a vector with the contrary direction
+        that.invertedVector = function() {
+            return [-this.dx, -this.dy];
+            };
+        // return a vector that is perpendicular to this line
+        that.perpendicularVector = function(clockwise) {
+            return clockwise ? [-this.dy, this.dx] : [this.dy, -this.dx];
+            };
+        // return vector distance
+        that.dist = function() {
+            return Math.sqrt(this.dx * this.dx + this.dy * this.dy);
+            };
+        // multiply vector with a ratio
+        that.scale = function(ratio) {
+            this.dx *= ratio;
+            this.dy *= ratio
+            return this;
+            };
+        // get/set vector length
+        that.length = function(length) {
+            var dist = this.dist();
+            if (length == null) {
+                return dist;
+                }
+            return this.scale(length/dist);
+            };
+        // return the slope
+        that.slope = function() {
+            return this.dx/this.dy;
+            };
+        // return a copy of this line
+        that.copy = function() {
+            return line(position(this.x, this.y), this.vector());
+            };
+        // invert direction
+        that.invert = function() {
+            this.vector(this.invertedVector);
+            return this;
+            };
+        // return a parallel line through a point (with the same vector)
+        that.parallel = function(p) {
+            return line(position(p.x, p.y), this.vector());
+            };
+        // return a perpendicular line from the origin (optionally from another point) with direction
+        that.perpendicular = function(p, clockwise) {
+            var point = (p == null || p.x == null)
+                ? position(this.x, this.y) : p;
+            return line(point, this.perpendicularVector(clockwise));
+            };
+        // return the intersection with a perpendicular line through a point
+        that.perpendicularPoint = function(p) {
+            return this.intersection(this.perpendicular(p));
+            };
+        // return perpendicular line from point
+        that.perpendicularLine = function(p) {
+            return line(p, this.perpendicularPoint(p));
+            };
+        // return point in mirrored position (with regard to this line)
+        that.mirror = function(p) {
+            var line = this.perpendicularLine(p);
+            return line.addEnd(line.vector());
+            };
+        // return a position by adding a vector/position/distance to origin
+        that.add = function(item) {
+            if (item == null) {
+                return this.origin();
+            } else if ($.isArray(item)) { // add a vector
+                return position(this.x + item[0], this.y + item[1])
+            } else if (item.x != null) { // add a position
+                return position(this.x + item.x, this.y + item.y);
+            } else if (typeof item === 'number' && isFinite(item)) { // add a distance
+                ratio = item/this.dist();
+                return position(this.x + this.dx*ratio, this.y + this.dy*ratio);
+            } else {
+                return this.origin();
+                }
+            };
+        // return a position by adding a vector/position/distance to end point
+        that.addEnd = function(item) {
+            return this.add(item).add(this.vector());
+            };
+        // end point on the line (pointed to by vector)
+        that.point = function(factor) {
+            if (factor == null) { factor = 1; }
+            var vector = [factor*this.dx, factor*this.dy];
+            return this.add(vector);
+            };
+        // midpoint on the line (half of vector distance, multiplied by factor)
+        that.mid = function(factor) {
+            return this.origin().mid(this.point(factor));
+            };
+        // factor of point (assuming it is on the line)
+        that.factor = function(p) {
+            return (dx === 0)
+                ? (p.y - this.y)/this.dy
+                : (p.x - this.x)/this.dx;
+            };
+        // intersection point with other line
+        that.intersection = function(line) {
+            var denominator = this.dy*line.dx - this.dx*line.dy
+            if (denominator === 0) { // parallel
+                return null; }
+            var num = this.dx*(line.y - this.y) + this.dy*(this.x - line.x);
+            return line.point(num/denominator);
+            };
+        return that;
+        };
+
+    /*
+     * Rectangle class
+     */
+    var rectangle = function(x, y, w, h) {
+        var that = {};
+        if (typeof x === "object") {
+            if (x instanceof jQuery) {
+                // jQuery object
+                var pos = x.offset();
+                that = {
+                    x : pos.left,
+                    y : pos.top,
+                    width : x.width(),
+                    height : x.height()
+                };
+            } else if (y == null) {
+                // assume x is rectangle
+                that = {
+                    x : parseFloat(x.x) || 0,
+                    y : parseFloat(x.y) || 0,
+                    width : parseFloat(x.width) || 0,
+                    height : parseFloat(x.height) || 0
+                };
+            } else {
+                // assume x and y are Position
+                that = {
+                    x : Math.min(x.x, y.x),
+                    y : Math.min(x.y, y.y),
+                    width : Math.abs(y.x - x.x),
+                    height : Math.abs(y.y - x.y)
+                };
+            }
+        } else {
+            that = {
+                x : parseFloat(x),
+                y : parseFloat(y),
+                width : parseFloat(w),
+                height : parseFloat(h)
+            };
+        }
+        // returns a copy of this Rectangle
+        that.copy = function() {
+            return rectangle(this);
+        };
+        // returns the position of this Rectangle
+        that.getPosition = function() {
+            return position(this);
+        };
+        // returns the size of this Rectangle
+        that.getSize = function() {
+            return size(this);
+        };
+        // returns the upper left corner position
+        that.getPt1 = that.getPosition;
+        // returns the lower right corner position of this Rectangle
+        that.getPt2 = function() {
+            return position({
+                x : this.x + this.width,
+                y : this.y + this.height
+            });
+        };
+        // sets the upper left corner position to pos
+        that.setPosition = function(pos) {
+            this.x = pos.x;
+            this.y = pos.y;
+            return this;
+        };
+        // adds pos to the position
+        that.setPt1 = that.setPosition; // TODO: not really the same
+        that.addPosition = function(pos) {
+            this.x += pos.x;
+            this.y += pos.y;
+            return this;
+        };
+        // adds pos to the dimensions
+        that.enlarge = function(pos) {
+            this.width += pos.x;
+            this.height += pos.y;
+            return this;
+        };
+        // sets the lower right corner to position pos
+        that.setPt2 = function(pos) {
+            this.width = pos.x - this.x;
+            this.height = pos.y - this.y;
+            return this;
+        };
+        // returns the center position of this Rectangle
+        that.getCenter = function() {
+            return position({
+                x : this.x + this.width / 2,
+                y : this.y + this.height / 2
+            });
+        };
+        // moves this rectangle's center to position pos
+        that.setCenter = function(pos) {
+            this.x = pos.x - this.width / 2;
+            this.y = pos.y - this.height / 2;
+            return this;
+        };
+        // returns true if both rectangles have equal position and size
+        that.equals = function(other) {
+            var eq = (this.x === other.x && this.y === other.y && this.width === other.width);
+            return eq;
+        };
+        // returns a rectangle with the difference width, height and position
+        that.delta = function(other) {
+            return rectangle(other.x - this.x, other.y - this.y, 
+            		other.width - this.width, other.height - this.height);
+        };
+        // returns the area of this Rectangle
+        that.getArea = function() {
+            return (this.width * this.height);
+        };
+        // returns the aspect ratio of this Rectangle
+        that.getAspect = function() {
+            return (this.width / this.height);
+        };
+        // eliminates negative width and height
+        that.normalize = function() {
+            var p = this.getPt2();
+            this.x = Math.min(this.x, p.x);
+            this.y = Math.min(this.y, p.y);
+            this.width = Math.abs(this.width);
+            this.height = Math.abs(this.height);
+            return this;
+        };
+        // returns if Position "pos" lies inside of this rectangle
+        that.containsPosition = function(pos) {
+            var ct = ((pos.x >= this.x) && (pos.y >= this.y)
+                    && (pos.x <= this.x + this.width) && (pos.y <= this.y
+                    + this.height));
+            return ct;
+        };
+        // returns true if rectangle "rect" is contained in this rectangle
+        that.containsRect = function(rect) {
+            return (this.containsPosition(rect.getPt1()) && this
+                    .containsPosition(rect.getPt2()));
+        };
+        // returns true if rectangle "rect" and this rectangle overlap
+        that.overlapsRect = function(rect) {
+            return this.intersect(rect) != null;
+        };
+        // returns the ratio of height to width
+        that.getProportion = function() {
+            return this.height/this.width;
+        };
+        // shrink/grow rectangle until it has the given proportion
+        that.setProportion = function(ratio, canGrow) {
+            var prop = this.getProportion();
+            if (ratio < prop == canGrow) {
+                this.width = this.height / ratio;
+            } else {
+                this.height = this.width * ratio;
+            }
+            return this;
+        };
+        // changes this rectangle's x/y values so it stays inside of rectangle
+        // "rect", keeping the proportions
+        that.stayInside = function(rect) {
+            this.x = Math.max(this.x, rect.x);
+            this.y = Math.max(this.y, rect.y);
+            if (this.x + this.width > rect.x + rect.width) {
+                this.x = rect.x + rect.width - this.width;
+            }
+            if (this.y + this.height > rect.y + rect.height) {
+                this.y = rect.y + rect.height - this.height;
+            }
+            return this;
+        };
+        // clips this rectangle so it stays inside of rectangle "rect"
+        that.clipTo = function(rect) {
+            var p1 = rect.getPt1();
+            var p2 = rect.getPt2();
+            var this2 = this.getPt2();
+            this.setPosition(position(Math.max(this.x, p1.x), Math.max(this.y, p1.y)));
+            this.setPt2(position(Math.min(this2.x, p2.x), Math.min(this2.y, p2.y)));
+            return this;
+        };
+        // returns the intersection of rectangle "rect" and this one
+        that.intersect = function(rect) {
+            var r = rect.copy();
+            var result = r.clipTo(this);
+            if (result.width < 0 || result.height < 0) result = null;
+            return result;
+        };
+
+        // returns a copy of rectangle "rect" that fits into this one
+        // (moving it first)
+        that.fit = function(rect) {
+            var r = rect.copy();
+            r.x = Math.max(r.x, this.x);
+            r.y = Math.max(r.y, this.x);
+            if (r.x + r.width > this.x + this.width) {
+                r.x = this.x + this.width - r.width;
+            }
+            if (r.y + r.height > this.y + this.height) {
+                r.y = this.y + this.height - r.height;
+            }
+            return r.intersect(this);
+        };
+
+        // adjusts position and size of jQuery element "$elem" to this rectangle
+        that.adjustDiv = function($elem) {
+            $elem.offset({
+                left : this.x,
+                top : this.y
+            });
+            $elem.width(this.width).height(this.height);
+        };
+        // returns position and size of this rectangle in css-compatible format
+        that.getAsCss = function() {
+            return {
+                left : this.x,
+                top : this.y,
+                width : this.width,
+                height : this.height
+            };
+        };
+        // returns position and size of this rectangle formatted for SVG attributes
+        that.getAsSvg = function() {
+            return [this.x, this.y, this.width, this.height].join(" ");
+        };
+        // returns if this rectangle is a rectangle
+        that.isRectangle = function () {
+        	return this.width > 0 && this.height > 0;
+        };
+        // returns size and position of this rectangle formatted for ??? (w x h@x,y)
+        that.toString = function() {
+            return this.width + "x" + this.height + "@" + this.x + "," + this.y;
+        };
+        return that;
+    };
+
+    /*
+     * Transform class
+     * 
+     * defines a class of affine transformations
+     */
+    var transform = function(spec) {
+        var that = {
+            m00 : 1.0,
+            m01 : 0.0,
+            m02 : 0.0,
+            m10 : 0.0,
+            m11 : 1.0,
+            m12 : 0.0,
+            m20 : 0.0,
+            m21 : 0.0,
+            m22 : 1.0
+        };
+        if (spec) {
+            jQuery.extend(that, spec);
+        }
+        ;
+        that.concat = function(trafA) {
+            // add Transform trafA to this Transform (i.e. this = trafC = trafA * this)
+            var trafC = {};
+            for ( var i = 0; i < 3; i++) {
+                for ( var j = 0; j < 3; j++) {
+                    var c = 0.0;
+                    for ( var k = 0; k < 3; k++) {
+                        c += trafA["m" + i + k] * this["m" + k + j];
+                    }
+                    trafC["m" + i + j] = c;
+                }
+            }
+            jQuery.extend(this, trafC);
+            return this;
+        };
+        that.transform = function(rect) {
+            // returns transformed Rectangle or Position with this Transform
+            // applied
+            var x = this.m00 * rect.x + this.m01 * rect.y + this.m02;
+            var y = this.m10 * rect.x + this.m11 * rect.y + this.m12;
+            var pt = position(x, y);
+            if (rect.width != null) {
+                // transform the other corner point
+                var pt2 = this.transform(rect.getPt2());
+                return rectangle(pt, pt2);
+            }
+            return pt;
+        };
+        that.invtransform = function(rect) {
+            // returns transformed Rectangle or Position with the inverse of
+            // this Transform applied
+            var det = this.m00 * this.m11 - this.m01 * this.m10;
+            var x = (this.m11 * rect.x - this.m01 * rect.y - this.m11
+                    * this.m02 + this.m01 * this.m12)
+                    / det;
+            var y = (-this.m10 * rect.x + this.m00 * rect.y + this.m10
+                    * this.m02 - this.m00 * this.m12)
+                    / det;
+            var pt = position(x, y);
+            if (rect.width != null) {
+                // transform the other corner point
+                var pt2 = this.invtransform(rect.getPt2());
+                return rectangle(pt, pt2);
+            }
+            return pt;
+        };
+        that.toString = function(pretty) {
+            var s = '[';
+            if (pretty)
+                s += '\n';
+            for ( var i = 0; i < 3; ++i) {
+                s += '[';
+                for ( var j = 0; j < 3; ++j) {
+                    if (j)
+                        s += ',';
+                    s += this['m' + i + j];
+                }
+                s += ']';
+                if (pretty)
+                    s += '\n';
+            }
+            s += ']';
+            if (pretty)
+                s += '\n';
+            return s;
+        };
+        // add class methods to instance
+        that.getRotation = transform.getRotation;
+        that.getRotationAround = transform.getRotationAround;
+        that.getTranslation = transform.getTranslation;
+        that.getMirror = transform.getMirror;
+        that.getScale = transform.getScale;
+
+        return that;
+    };
+
+    transform.getRotation = function(angle) {
+        // returns a Transform that is a rotation by angle degrees around [0,0]
+        if (angle !== 0) {
+            var t = Math.PI * parseFloat(angle) / 180.0;
+            var cost = Math.cos(t);
+            var sint = Math.sin(t);
+            var traf = {
+                m00 : cost,
+                m01 : -sint,
+                m10 : sint,
+                m11 : cost
+            };
+            return transform(traf);
+        }
+        return transform();
+    };
+
+    transform.getRotationAround = function(angle, pos) {
+        // returns a Transform that is a rotation by angle degrees around pos
+        var traf = transform.getTranslation(pos.neg());
+        traf.concat(transform.getRotation(angle));
+        traf.concat(transform.getTranslation(pos));
+        return traf;
+    };
+
+    transform.getTranslation = function(pos) {
+        // returns a Transform that is a translation by [pos.x, pos,y]
+        var traf = {
+            m02 : pos.x,
+            m12 : pos.y
+        };
+        return transform(traf);
+    };
+
+    transform.getMirror = function(type) {
+        // returns a Transform that is a mirror about the axis type
+        if (type === 'x') {
+            var traf = {
+                m00 : 1,
+                m11 : -1
+            };
+        } else {
+            var traf = {
+                m00 : -1,
+                m11 : 1
+            };
+        }
+        return transform(traf);
+    };
+
+    transform.getScale = function(size) {
+        // returns a Transform that is a scale by [size.width, size.height]
+        var traf = {
+            m00 : size.width,
+            m11 : size.height
+        };
+        return transform(traf);
+    };
+
+    // export constructor functions to digilib plugin
+    var geometry = {
+            size : size,
+            position : position,
+            line : line,
+            rectangle : rectangle,
+            transform : transform
+    };
+    
+    // install function called by digilib on plugin object
+    var install = function() {
+        // add constructor object to fn
+        this.fn.geometry = geometry;
+    };
+    
+    // digilib plugin object
+    var plugin = {
+            name : 'geometry',
+            install : install,
+            fn : {},
+    };
+
+    // plug into digilib
+    if ($.fn.digicat == null) {
+        $.error("jquery.digicat.geometry must be loaded after jquery.digicat!");
+    } else {
+        $.fn.digicat('plugin', plugin);
+    }
+})(jQuery);
--- a/webapp/src/main/webapp/jquery/jquery.digicat.js	Fri Feb 26 17:44:52 2016 +0100
+++ b/webapp/src/main/webapp/jquery/jquery.digicat.js	Fri Feb 26 18:57:43 2016 +0100
@@ -50,17 +50,33 @@
         'scalerBaseUrl' : null,
         // prefix of digilib classes in CSS styles
         'cssPrefix' : 'dl-',
-        // Scaler parameter defaults
-        'pn' : 1,
+        // parameter defaults
+        'pg' : 1,
+        'rows' : 5,
+        'cols' : 4,
+        // list of digicat parameters
+        'digicatParamNames' : [ 'fn', 'pg', 'rows', 'cols' ],
         // list of additional parameters (for page outside of digilib)
         'additionalParamNames' : [],
         // list of parameters to suppress when generating page URL
         'suppressParamNames' : null,
         // list of Scaler parameters
-        'scalerParamNames' : ['fn','pn','dw','dh','ww','wh','wx','wy','ws','mo',
-                              'rot','cont','brgt','rgbm','rgba','ddpi','ddpix','ddpiy','colop'],
+        'scalerParamNames' : ['fn', 'pn', 'dw', 'dh', 'ww', 'wh', 'wx', 'wy', 'ws', 'mo', 'rot', 'cont', 'brgt', 'rgbm', 'rgba',
+                'ddpi', 'ddpix', 'ddpiy', 'colop'],
+        // list of digilib parameters
+        'digilibParamNames' : ['fn','pn','ww','wh','wx','wy','ws','mo','rot','cont','brgt','rgbm','rgba','ddpi','colop','mk','clop'],
         // reserved space in full page display (default value accounts for body margins)
-        'pageInsets' : { 'x' : 26, 'y': 20 }
+        'pageInsets' : {
+            'x' : 26,
+            'y' : 20
+        },
+        // reserved space for each image
+        'imageInsets' : {
+            'x' : 4,
+            'y' : 20
+        },
+        // TODO: do we need this?
+        'interactionMode' : 'fullscreen'
     };
 
     // affine geometry plugin stub
@@ -82,7 +98,7 @@
         init : function(options) {
             // import geometry classes
             if (plugins.geometry == null) {
-                $.error("jquery.digilib.geometry plugin not found!");
+                $.error("jquery.digicat.geometry plugin not found!");
             } else {
                 // geometry plugin puts classes in the shared fn
                 geom = fn.geometry;
@@ -95,7 +111,7 @@
             queryParams = parseQueryParams();
             // filter additional parameters
             for ( var p in queryParams) {
-                if ($.inArray(p, settings.digilibParamNames) < 0) {
+                if ($.inArray(p, settings.digicatParamNames) < 0) {
                     settings.additionalParamNames.push(p);
                 }
             }
@@ -126,6 +142,10 @@
                     elemSettings = data.settings;
                 }
                 unpackParams(data);
+                // list of current insets (dynamic for buttons etc.)
+                data.currentInsets = {
+                    'static' : elemSettings.pageInsets
+                };
                 // check digilib base URL
                 if (elemSettings.digilibBaseUrl == null) {
                     // take current host
@@ -168,7 +188,41 @@
                 // send setup event
                 $(data).trigger('setup');
             });
-        }
+        },
+
+        /**
+         * goto given page nr (+/-: relative)
+         * 
+         * @param data
+         * @param pageNr
+         * @returns {Boolean}
+         */
+        gotoPage : function(data, pageNr) {
+            var settings = data.settings;
+            var oldpg = settings.pg;
+            if (pageNr == null) {
+                pageNr = window.prompt("Goto page number", oldpg);
+            }
+            var pg = setNumValue(settings, "pg", pageNr);
+            if (pg == null)
+                return false; // nothing happened
+            if (pg < 1) {
+                alert("no such page (page number too low)");
+                settings.pg = oldpg;
+                return false;
+            }
+            // TODO: how do we get pt?
+            if (settings.pt != null) {
+                if (pg > settings.pt) {
+                    alert("no such page (page number too high)");
+                    settings.pg = oldpg;
+                    return false;
+                }
+            }
+            // then reload
+            redisplay(data);
+        },
+
     };
 
     /**
@@ -218,13 +272,14 @@
         // retrieveOptions(data);
     };
 
-    /** put objects back into parameters
+    /**
+     * put objects back into parameters
      * 
      */
-    var packParams = function (data) {
+    var packParams = function(data) {
         var settings = data.settings;
         // store user interface options in cookie
-        //storeOptions(data);
+        // storeOptions(data);
         // trigger pack handlers
         $(data).trigger('pack');
     };
@@ -254,12 +309,12 @@
         return geom.size(imgW, imgH);
     };
 
-    /** 
-     * return a query string from key names from a parameter hash 
-     * (ignores keys if the same value is in defaults)
+    /**
+     * return a query string from key names from a parameter hash (ignores keys
+     * if the same value is in defaults)
      * 
      */
-    var getParamString = function (settings, keys, defaults) {
+    var getParamString = function(settings, keys, defaults) {
         var paramString = '';
         var nx = false;
         for (var i = 0; i < keys.length; ++i) {
@@ -279,21 +334,103 @@
     };
 
     /** 
+     * returns URL and query string for current digicat.
+     * if digilibPage != null returns URL to page in digilib installation with digilib parameters,
+     * otherwise using current URL and parameters.
+     * 
+     */
+    var getDigicatUrl = function (data, digilibPage) {
+        packParams(data);
+        var settings = data.settings;
+        var paramList = settings.digicatParamNames;
+        if (digilibPage != null) {
+            var baseUrl = data.settings.digilibBaseUrl + digilibPage;
+        } else {
+            paramList = settings.additionalParamNames.concat(settings.digicatParamNames);
+            if (settings.suppressParamNames != null) {
+                // eliminate suppressed parameters from list
+                paramList = $.map(paramList, function(e, idx) {
+                    if ($.inArray(e, settings.suppressParamNames) >= 0) {
+                        return null;
+                    } else {
+                        return e;
+                    }
+                });
+            }
+            // take url from current location
+            var baseUrl = window.location.href;
+            var pos = baseUrl.indexOf('?');
+            if (pos > -1) {
+                baseUrl = baseUrl.substring(0, pos);
+            }
+        }
+        var queryString = getParamString(settings, paramList, defaults);
+        return baseUrl + '?' + queryString;
+    };
+
+    /** 
+     * returns URL and query string for current digilib.
+     * if digilibPage != null returns URL to page in digilib installation with digilib parameters,
+     * otherwise using current URL and parameters.
+     * 
+     */
+    var getDigilibUrl = function (data, digilibPage) {
+        packParams(data);
+        var settings = data.settings;
+        var paramList = settings.digilibParamNames;
+        if (digilibPage != null) {
+            var baseUrl = data.settings.digilibBaseUrl + digilibPage;
+        } else {
+            paramList = settings.additionalParamNames.concat(settings.digilibParamNames);
+            if (settings.suppressParamNames != null) {
+                // eliminate suppressed parameters from list
+                paramList = $.map(paramList, function(e, idx) {
+                    if ($.inArray(e, settings.suppressParamNames) >= 0) {
+                        return null;
+                    } else {
+                        return e;
+                    }
+                });
+            }
+            // take url from current location
+            var baseUrl = window.location.href;
+            var pos = baseUrl.indexOf('?');
+            if (pos > -1) {
+                baseUrl = baseUrl.substring(0, pos);
+            }
+        }
+        var queryString = getParamString(settings, paramList, defaults);
+        return baseUrl + '?' + queryString;
+    };
+
+    /**
      * returns URL and query string for Scaler
      */
-    var getScalerUrl = function (data) {
+    var getScalerUrl = function(data) {
         packParams(data);
         var settings = data.settings;
         if (settings.scalerBaseUrl == null) {
             alert("ERROR: URL of digilib Scaler servlet missing!");
-            }
+        }
         var keys = settings.scalerParamNames;
         var queryString = getParamString(settings, keys, defaults);
         var url = settings.scalerBaseUrl + '?' + queryString;
         return url;
     };
 
-    var handleScalerImgError = function (data, evt, a, b) {
+    /**
+     * (re)load the page with the current settings.
+     * 
+     */
+    var redisplay = function(data) {
+        var settings = data.settings;
+        // update location.href (browser URL) in fullscreen mode
+        var url = getDigicatUrl(data);
+        // reload window
+        window.location = url;
+    };
+
+    var handleScalerImgError = function(data, evt, a, b) {
         console.error("error loading scaler image:", evt);
         $(data).trigger('imgerror');
     };
@@ -307,29 +444,39 @@
         var cssPrefix = settings.cssPrefix;
         $elem.addClass(cssPrefix + 'digicat');
         var scalerUrl;
-        var rows = 4;
-        var cols = 5;
-        var pn = 1;
+        var dlUrl;
+        var pg = settings.pg;
+        var rows = settings.rows;
+        var cols = settings.cols;
+        var pn = (pg - 1) * rows * cols + 1;
         var tblSize = getFullscreenImgSize(data);
-        tblSize.width -= settings.pageInsets.x;
-        tblSize.height -= settings.pageInsets.y;
-        settings.dw = Math.floor(tblSize.width / cols);
-        settings.dh = Math.floor(tblSize.height / rows);
+        var tdw = Math.floor(tblSize.width / cols);
+        var tdh = Math.floor(tblSize.height / rows);
+        settings.dw = tdw - settings.imageInsets.x;
+        settings.dh = tdh - settings.imageInsets.y;
         var $tbl = $('<table/>');
         var $tr;
         var $td;
+        var $link;
         var $img;
-        for (var ridx=0; ridx < rows; ++ridx) {
+        for (var ridx = 0; ridx < rows; ++ridx) {
             $tr = $('<tr/>');
-            for (var cidx=0; cidx < cols; ++cidx) {
+            for (var cidx = 0; cidx < cols; ++cidx) {
                 $td = $('<td>');
+                settings.pn = pn;
+                /*
+                 * link
+                 */
+                dlUrl = getDigilibUrl(data, '/digilib.html');
+                $link = $('<a href="'+dlUrl+'" target="_blank"/>');
                 /*
                  * scaler image
                  */
                 $img = $('<img/>');
-                $img.addClass(cssPrefix+'pic');
-                $img.on('error', function (evt, a, b) { handleScalerImgError(data, evt, a, b); });
-                settings.pn = pn;
+                $img.addClass(cssPrefix + 'pic');
+                $img.on('error', function(evt, a, b) {
+                    handleScalerImgError(data, evt, a, b);
+                });
                 scalerUrl = getScalerUrl(data);
                 $img.attr('src', scalerUrl);
                 /*
@@ -340,21 +487,56 @@
                 /*
                  * assemble element
                  */
-                $td.append($img);
-                $td.append($cap);
+                $td.css('width', tdw + 'px');
+                $td.css('height', tdh + 'px');
+                $link.append($img);
+                $link.append($cap);
+                $td.append($link);
                 $tr.append($td);
                 pn += 1;
             }
             $tbl.append($tr);
         }
-        // create new inner html, keeping buttons and content marked with "keep" class
-        $elem.contents(':not(.'+cssPrefix+'keep)').remove();
+        // create new inner html, keeping buttons and content marked with class "keep"
+        $elem.contents(':not(.' + cssPrefix + 'keep)').remove();
         // scaler should be the first child element?
         $elem.prepend($tbl);
         data.$tbl = $tbl;
     };
 
     /**
+     * sets a key to a value (relative values with +/- if relative=true).
+     * 
+     */
+    var setNumValue = function(settings, key, value) {
+        if (value == null)
+            return null;
+        if (isNumber(value)) {
+            settings[key] = value;
+            return value;
+        }
+        var sign = value[0];
+        if (sign === '+' || sign === '-') {
+            if (settings[key] == null) {
+                // this isn't perfect but still...
+                settings[key] = 0;
+            }
+            settings[key] = parseFloat(settings[key]) + parseFloat(value);
+        } else {
+            settings[key] = value;
+        }
+        return settings[key];
+    };
+
+    /**
+     * return if the argument is a number. from Douglas Crockford, A.10. this is
+     * different from $.isNumeric().
+     */
+    var isNumber = function(value) {
+        return typeof value === 'number' && isFinite(value);
+    };
+
+    /**
      * functions to export to plugins.
      */
     fn = {
--- a/webapp/src/main/webapp/jquery/jquery.digilib.geometry.js	Fri Feb 26 17:44:52 2016 +0100
+++ b/webapp/src/main/webapp/jquery/jquery.digilib.geometry.js	Fri Feb 26 18:57:43 2016 +0100
@@ -804,12 +804,7 @@
 
     // plug into digilib
     if ($.fn.digilib == null) {
-        if ($.fn.digicat == null) {
-            $.error("jquery.digilib.geometry must be loaded after jquery.digilib!");
-        } else {
-            // plug into digicat (somewhat ugly)
-            $.fn.digicat('plugin', plugin);
-        }
+        $.error("jquery.digilib.geometry must be loaded after jquery.digilib!");
     } else {
         $.fn.digilib('plugin', plugin);
     }