view client/digitallibrary/jquery/jquery.digilib.js @ 786:868c2e795aca jquery

new plugin architecture.
author robcast
date Thu, 17 Feb 2011 14:32:48 +0100
parents b9a75079aece
children b322f553f92e
line wrap: on
line source

/* Copyright (c) 2011 Martin Raspe, Robert Casties
 
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 2 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 Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
Authors:
  Martin Raspe, Robert Casties, 11.1.2011
*/

/**
 * digilib jQuery plugin
**/ 

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

// fallback for console.log calls
if (typeof(console) === 'undefined') {
    var console = {
        log : function(){}, 
        debug : function(){}, 
        error : function(){}
        };
    var customConsole = false; // set to true if debugging for MS IE
}

(function($) {
    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 image",
            icon : "back.png"
            },
        fwd : {
            onclick : ["gotoPage", "+1"],
            tooltip : "goto next image",
            icon : "fwd.png"
            },
        page : {
            onclick : "gotoPage",
            tooltip : "goto image number",
            icon : "page.png"
            },
        bird : {
            onclick : "showBirdDiv",
            tooltip : "show bird's eye view",
            icon : "birds-eye.png"
            },
        help : {
            onclick : "showAboutDiv",
            tooltip : "about Digilib",
            icon : "help.png"
            },
        reset : {
            onclick : "reset",
            tooltip : "reset image",
            icon : "reset.png"
            },
        mark : {
            onclick : "setMark",
            tooltip : "set a mark",
            icon : "mark.png"
            },
        delmark : {
            onclick : "removeMark",
            tooltip : "delete the last mark",
            icon : "delmark.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 : "javascript:setParamWin('rgb', '...')",
            tooltip : "set rgb values",
            icon : "rgb.png"
            },
        quality : {
            onclick : "setQuality",
            tooltip : "set image quality",
            icon : "quality.png"
            },
        size : {
            onclick : "javascript:toggleSizeMenu()",
            tooltip : "set page size",
            icon : "size.png"
            },
        calibrationx : {
            onclick : "calibrate",
            tooltip : "calibrate screen resolution",
            icon : "calibration-x.png"
            },
        scale : {
            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 : "options.png"
            },
        lessoptions : {
            onclick : ["moreButtons", "-1"],
            tooltip : "less options",
            icon : "options.png"
            },
        SEP : {
            icon : "sep.png"
            }
        };

    var defaults = {
        // version of this script
        'version' : 'jquery.digilib.js 0.9',
        // logo url
        'logoUrl' : '../img/digilib-logo-text1.png',
        // homepage url (behind logo)
        'homeUrl' : 'http://digilib.berlios.de',
        // base URL to digilib viewer (for reference URLs)
        'digilibBaseUrl' : null,
        // base URL to Scaler servlet
        'scalerBaseUrl' : null,
        // list of Scaler parameters
        'scalerParamNames' : ['fn','pn','dw','dh','ww','wh','wx','wy','ws','mo',
                              'rot','cont','brgt','rgbm','rgba','ddpi','ddpix','ddpiy'],
        // Scaler parameter defaults
        'pn' : 1,
        'ww' : 1.0,
        'wh' : 1.0,
        'wx' : 0.0,
        'wy' : 0.0,
        'ws' : 1.0,
        'mo' : '',
        'rot' : 0,
        'cont' : 0,
        'brgt' : 0,
        'rgbm' : '0/0/0',
        'rgba' : '0/0/0',
        'ddpi' : null,
        'ddpix' : null,
        'ddpiy' : null,
        // list of digilib parameters
        'digilibParamNames' : ['fn','pn','ww','wh','wx','wy','ws','mo','rot','cont','brgt','rgbm','rgba','ddpi','mk','clop'],
        // digilib parameter defaults
        'mk' : '',
        'clop' : '',
        // mode of operation: 
        // fullscreen = take parameters from page URL, keep state in page URL
        // embedded = take parameters from Javascript options, keep state inside object 
        'interactionMode' : 'fullscreen',
        // buttons
        'buttons' : buttons,
        // defaults for digilib buttons
        'buttonSettings' : {
            'fullscreen' : {
                // path to button images (must end with a slash)
                'imagePath' : 'img/fullscreen/',
                'standardSet' : ["reference","zoomin","zoomout","zoomarea","zoomfull","pagewidth","back","fwd","page","bird","help","reset","toggleoptions"],
                'specialSet' : ["mark","delmark","hmir","vmir","rot","brgt","cont","rgb","quality","size","calibrationx","scale","toggleoptions"],
                'buttonSets' : ['standardSet', 'specialSet']
                },
            'embedded' : {
                'imagePath' : 'img/embedded/16/',
                'standardSet' : ["reference","zoomin","zoomout","zoomarea","zoomfull","bird","help","reset","toggleoptions"],
                'specialSet' : ["mark","delmark","hmir","vmir","rot","brgt","cont","rgb","quality","scale","toggleoptions"],
                'buttonSets' : ['standardSet', 'specialSet']
                }
        },

        // number of visible button groups
        'visibleButtonSets' : 1,
        // is birdView shown?
        'isBirdDivVisible' : false,
        // dimensions of bird's eye div
        'birdDivWidth' : 200, 
        'birdDivHeight' : 200,
        // parameters used by bird's eye div
        'birdDivParams' : ['fn','pn','dw','dh'],
        // is the "about" window shown?
        'isAboutDivVisible' : false,
        // maximum width of background image for drag-scroll
        'maxBgSize' : 10000,
        // space to be left free in full page display, default value is for scrollbar
        'scalerInset' : 10
        };

    // list of plugins
    var plugins = {};
    
    // affine geometry plugin stub
    var geom;

    var FULL_AREA;

    var actions = {
        // init: digilib initialization
        init : function(options) {
            // import geometry classes
            if (plugins.geometry == null) {
                $.error("jquery.digilib.geometry plugin not found!");
                geom = dlGeometry();
            } else {
                geom = plugins.geometry.init();
            }
            FULL_AREA  = geom.rectangle(0, 0, 1, 1);
            
            // settings for this digilib instance are merged from defaults and options
            var settings = $.extend({}, defaults, options);
            var isFullscreen = settings.interactionMode === 'fullscreen';
            var queryParams = {};
            if (isFullscreen) {
                queryParams = parseQueryParams();
                // check scalerBaseUrl
                if (settings.scalerBaseUrl == null) {
                    // try the host this came from
                    var h = window.location.host;
                    if (window.location.host) {
                        var url = window.location.href;
                        // assume the page lives in [webapp]/jquery/
                        var pos = url.indexOf('jquery/');
                        if (pos > 0) {
                            settings.scalerBaseUrl = url.substring(0, pos) + 'servlet/Scaler';
                        }
                    }
                }
            }
            return this.each(function() {
                var $elem = $(this);
                var data = $elem.data('digilib');
                var params, elemSettings;
                // if the plugin hasn't been initialized yet
                if (!data) {
                    // merge query parameters
                    if (isFullscreen) {
                        params = queryParams;
                    } else {
                        params = parseImgParams($elem);
                        if ($.cookie) {
                            // retrieve params from cookie
                            var ck = "digilib-embed:fn:" + escape(params.fn) + ":pn:" + (params.pn || '1');
                            var cs = $.cookie(ck);
                            console.debug("get cookie=", ck, " value=", cs);
                            if (cs) {
                                var cp = parseQueryString(cs);
                                // ignore fn and pn from cookie TODO: should we keep pn?
                                delete cp.fn;
                                delete cp.pn;
                                $.extend(params, cp);
                            }
                        }
                    }
                    // store $(this) element in the settings
                    elemSettings = $.extend({}, settings, params);
                    data = {
                            $elem : $elem,
                            settings : elemSettings,
                            queryParams : params
                    };
                    // store in data element
                    $elem.data('digilib', data);
                }
                unpackParams(data);
                // check if browser knows *background-size
                for (var bs in {'':1, '-moz-':1, '-webkit-':1, '-o-':1}) {
                    if ($elem.css(bs+'background-size')) {
                        data.hasBgSize = true;
                        data.bgSizeName = bs+'background-size';
                        break;
                    }
                }
                // check digilib base URL
                if (elemSettings.digilibBaseUrl == null) {
                    if (isFullscreen) {
                        // take current host
                        var url = window.location.toString();
                        var pos = url.indexOf('?');
                        elemSettings.digilibBaseUrl = url.substring(0, pos);
                    } else {
                        var url = elemSettings.scalerBaseUrl;
                        if (url) {
                            // build it from scaler URL
                            var bp = url.indexOf('/servlet/Scaler');
                            elemSettings.digilibBaseUrl = url.substring(0, bp) + '/digilib.jsp';
                        }
                    }
                }
                // initialise plugins
                for (p in plugins) {
                    plugins[p].init(data);
                }
                // get image info from server if needed
                if (data.scaleMode === 'pixel' || data.scaleMode === 'size') {
                    loadImageInfo(data, updateDisplay); // updateDisplay(data) on completion
                }
                // create buttons before scaler 
                for (var i = 0; i < elemSettings.visibleButtonSets; ++i) {
                    showButtons(data, true, i);
                    }
                // create HTML structure for scaler, taking width of buttons div into account
                setupScalerDiv(data);
                highlightButtons(data);
                // bird's eye view creation
                if (elemSettings.isBirdDivVisible) {
                    setupBirdDiv(data);
                    data.$birdDiv.show();
                    }
                // about window creation - TODO: could be deferred? restrict to only one item?
                setupAboutDiv(data);
                // drag zoom area around in scaler div 
                // setupZoomDrag(data); // is done in scalerImgLoadedHandler()
            });
        },

        // destroy: clean up digilib
        destroy : function(data) {
            return this.each(function(){
                var $elem = $(this);
                $(window).unbind('.digilib'); // unbind all digilibs(?)
                data.digilib.remove();
                $elem.removeData('digilib');
            });
        },

        // show or hide the 'about' window
        showAboutDiv : function(data, show) {
            var on = showDiv(data.settings.isAboutDivVisible, data.$aboutDiv, show);
            data.settings.isAboutDivVisible = on;
            highlightButtons(data, 'help', on);
        },

        // event handler: toggles the visibility of the bird's eye window 
        showBirdDiv : function (data, show) {
            var settings = data.settings;
            if (data.$birdDiv == null) {
                // no bird div -> create
                setupBirdDiv(data);
            }
            var on = showDiv(settings.isBirdDivVisible, data.$birdDiv, show);
            settings.isBirdDivVisible = on;
            highlightButtons(data, 'bird', on);
            updateBirdDiv(data);
            storeOptions(data);
        },

        // goto given page nr (+/-: relative)
        gotoPage : function (data, pageNr) {
            var settings = data.settings;
            var oldpn = settings.pn;
            if (pageNr == null) {
                pageNr = window.prompt("Goto page number", oldpn);
            }
            var pn = setNumValue(settings, "pn", pageNr);
            if (pn == null) return false; // nothing happened
            if (pn < 1) {
                alert("no such page (page number too low)");
                settings.pn = oldpn;
                return false;
                }
            // TODO: how do we get pt?
            if (settings.pt) {
                if (pn > settings.pt) {
                    alert("no such page (page number too high)");
                    settings.pn = oldpn;
                    return false;
                    }
                }
            // reset mk and others(?)
            data.marks = [];
            data.zoomArea = FULL_AREA;
            // then reload
            redisplay(data);
        },

        // zoom by a given factor
        zoomBy : function (data, factor) {
            zoomBy(data, factor);
        },

        // zoom to area (or interactive)
        zoomArea : function (data, area) {
            var settings = data.settings;
            if (area == null) {
                // interactively
                zoomArea(data);
            } else {
                data.zoomArea = geom.rectangle(area);
                redisplay(data);
            }
        },

        // zoom out to full page
        zoomFull : function (data, mode) {
            data.zoomArea = FULL_AREA;
            if (mode === 'width') {
                data.dlOpts.fitwidth = 1;
                delete data.dlOpts.fitheight;
            } else if (mode === 'height') {
                data.dlOpts.fitheight = 1;
                delete data.dlOpts.fitwidth;
            } else {
                delete data.dlOpts.fitwidth;
                delete data.dlOpts.fitheight;
            }
            redisplay(data);
        },

        // set a mark by clicking (or giving a position)
        setMark : function (data, mpos) {
            if (mpos == null) {
                // interactive
                setMark(data);
            } else {
                // use position
                data.marks.push(pos);
                redisplay(data);
            }
        },

        // remove the last mark
        removeMark : function (data) {
            data.marks.pop();
            redisplay(data);
        },

        // mirror the image
        mirror : function (data, mode) {
            var flags = data.scalerFlags;
            if (mode === 'h') {
                if (flags.hmir) {
                    delete flags.hmir;
                } else {
                    flags.hmir = 1;
                }
            } else {
                if (flags.vmir) {
                    delete flags.vmir;
                } else {
                    flags.vmir = 1;
                }
            }
            redisplay(data);
        },

        // rotate the image
        rotate : function (data, angle) {
            var rot = data.settings.rot;
            if (angle == null) {
                angle = window.prompt("Rotation angle:", rot);
            }
            data.settings.rot = angle;
            redisplay(data);
        },

        // change brightness
        brightness : function (data, factor) {
            var brgt = data.settings.brgt;
            if (factor == null) {
                factor = window.prompt("Brightness (-255..255)", brgt);
            }
            data.settings.brgt = factor;
            redisplay(data);
        },

        // change contrast
        contrast : function (data, factor) {
            var cont = data.settings.cont;
            if (factor == null) {
                factor = window.prompt("Contrast (-8, 8)", cont);
            }
            data.settings.cont = factor;
            redisplay(data);
        },

        // 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++;
                }
            }
            // persist setting
            storeOptions(data);
        },

        // reset image parameters to defaults
        reset : function (data) {
            var settings = data.settings;
            var paramNames = settings.digilibParamNames;
            var params = data.queryParams;
            // delete all digilib parameters
            for (var i = 0; i < paramNames.length; i++) {
                var paramName = paramNames[i];
                delete settings[paramName];
                }
            settings.fn = params.fn || ''; // no default defined
            settings.pn = params.pn || defaults.pn;
            settings.dw = params.dw;
            settings.dh = params.dh;
            settings.isBirdDivVisible = false;
            settings.visibleButtonSets = 1;
            // resets zoomArea, marks, scalerflags
            data.zoomArea = FULL_AREA;
            data.marks = [];
            data.scalerFlags = {};
            delete data.dlOpts.fitwidth;
            delete data.dlOpts.fitheight;
            redisplay(data);
        },

        // presents a reference url (returns value if noprompt)
        reference : function (data, noprompt) {
            var settings = data.settings;
            var url = getDigilibUrl(data);
            if (noprompt == null) {
                window.prompt("URL reference to the current view", url);
            }
            return url;
        },

        // set image quality
        setQuality : function (data, qual) {
            var oldq = getQuality(data);
            if (qual == null) {
                qual = window.prompt("Image quality (0..2)", oldq);
            }
            qual = parseInt(qual, 10);
            if (qual >= 0 && qual <= 2) {
                setQuality(data, qual);
                redisplay(data);
            }
        },

        // calibrate (only faking)
        calibrate : function (data, res) {
            var oldRes = data.settings.ddpi;
            if (res == null) {
                res = window.prompt("Display resolution (dpi)", oldRes);
            }
            if (res != null) {
                data.settings.ddpi = res;
                redisplay(data);
            }
        },

        // set image scale mode
        setScaleMode : function (data, mode) {
            var oldM = getScaleMode(data);
            if (mode == null) {
                mode = window.prompt("Image scale mode (screen, pixel, size)", oldM);
            }
            if (mode != null) {
                setScaleMode(data, mode);
                data.scaleMode = mode;
                redisplay(data);
            }
        }

    // end of actions
    };

    // returns parameters from page url
    var parseQueryParams = function() {
        return parseQueryString(window.location.search.slice(1));
    };

    // returns parameters from embedded img-element
    var parseImgParams = function($elem) {
        var src = $elem.find('img').first().attr('src');
        if (!src) return null;
        var pos = src.indexOf('?');
        var query = (pos < 0) ? '' : src.substring(pos + 1);
        var scalerUrl = src.substring(0, pos);
        var params = parseQueryString(query);
        params.scalerBaseUrl = scalerUrl;
        return params;
    };

    // parses query parameter string into parameter object
    var parseQueryString = function(query) {
        var params = {};
        if (query == null) return params;
        var pairs = query.split("&");
        //var keys = [];
        for (var i = 0; i < pairs.length; i++) {
            var pair = pairs[i].split("=");
            if (pair.length === 2) {
                params[pair[0]] = pair[1];
                //keys.push(pair[0]);
            }
        }
        return params;
    };

    // returns a query string from key names from a parameter hash (ignoring if the same value is in defaults)
    var getParamString = function (settings, keys, defaults) {
        var paramString = '';
        var nx = false;
        for (i = 0; i < keys.length; ++i) {
            var key = keys[i];
            if ((settings[key] != null) && ((defaults == null) || (settings[key] != defaults[key]))) {
                // first param gets no '&'
                if (nx) {
                    paramString += '&';
                } else {
                    nx = true;
                }
                // add parm=val
                paramString += key + '=' + settings[key];
            }
        }
        return paramString;
    };

    // returns URL and query string for Scaler
    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;
    };

    // returns URL for bird's eye view image
    var getBirdImgUrl = function (data, moreParams) {
        var settings = data.settings;
        var birdDivOptions = {
                dw : settings.birdDivWidth,
                dh : settings.birdDivHeight
        };
        var birdSettings = $.extend({}, settings, birdDivOptions);
        // use only the relevant parameters
        if (moreParams == null) {
            var params = getParamString(birdSettings, settings.birdDivParams, defaults);
        } else {
            // filter scaler flags
            if (birdSettings.mo != null) {
                var mo = '';
                if (data.scalerFlags.hmir != null) {
                    mo += 'hmir,';
                }
                if (data.scalerFlags.vmir != null) {
                    mo += 'vmir';
                }
                birdSettings.mo = mo;
            }
            var params = getParamString(birdSettings, 
                    settings.birdDivParams.concat(moreParams), defaults);
        }
        var url = settings.scalerBaseUrl + '?' + params;
        return url;
    };

    // returns URL and query string for current digilib
    var getDigilibUrl = function (data) {
        packParams(data);
        var settings = data.settings;
        var queryString = getParamString(settings, settings.digilibParamNames, defaults);
        return settings.digilibBaseUrl + '?' + queryString;
    };

    // loads image information from digilib server via HTTP (and calls complete-fn)
    var loadImageInfo = function (data, complete) {
        var settings = data.settings;
        var p = settings.scalerBaseUrl.indexOf('/servlet/Scaler');
        var url = settings.scalerBaseUrl.substring(0, p) + '/ImgInfo-json.jsp';
        url += '?' + getParamString(settings, ['fn', 'pn'], defaults);
        // TODO: better error handling
        $.getJSON(url, function (json) {
            console.debug("got json data=", json);
            data.imgInfo = json;
            if (complete != null) {
                complete.call(this, data, json);
            }
        });
    };

    // processes some parameters into objects and stuff
    var unpackParams = function (data) {
        var settings = data.settings;
        // zoom area
        var zoomArea = geom.rectangle(settings.wx, settings.wy, settings.ww, settings.wh);
        data.zoomArea = zoomArea;
        // marks
        var marks = [];
        if (settings.mk) {
            var mk = settings.mk;
            if (mk.indexOf(";") >= 0) {
                var pa = mk.split(";");    // old format with ";"
            } else {
                var pa = mk.split(",");    // new format
            }
            for (var i = 0; i < pa.length ; i++) {
                var pos = pa[i].split("/");
                if (pos.length > 1) {
                    marks.push(geom.position(pos[0], pos[1]));
                    }
                }
            }
        data.marks = marks;
        // mo (Scaler flags)
        var flags = {};
        if (settings.mo) {
            var pa = settings.mo.split(",");
            for (var i = 0; i < pa.length ; i++) {
                flags[pa[i]] = pa[i];
                }
            }
        data.scalerFlags = flags;
        data.scaleMode = getScaleMode(data);
        retrieveOptions(data);
    };

    // put objects back into parameters
    var packParams = function (data) {
        var settings = data.settings;
        // zoom area
        if (data.zoomArea) {
            settings.wx = cropFloat(data.zoomArea.x);
            settings.wy = cropFloat(data.zoomArea.y);
            settings.ww = cropFloat(data.zoomArea.width);
            settings.wh = cropFloat(data.zoomArea.height);
            }
        // marks
        if (data.marks) {
            settings.mk = '';
            for (var i = 0; i < data.marks.length; i++) {
                if (i) {
                    settings.mk += ',';
                    }
                settings.mk += cropFloat(data.marks[i].x).toString() +
                    '/' + cropFloat(data.marks[i].y).toString();
                }
            }
        // Scaler flags
        if (data.scalerFlags) {
            var mo = '';
            for (var f in data.scalerFlags) {
                if (mo) {
                    mo += ',';
                }
                mo += f;
            }
            settings.mo = mo;
        }
        // user interface options
        storeOptions(data);
    };

    var storeOptions = function (data) {
        // save digilib options in cookie
        var settings = data.settings;
        if (data.dlOpts) {
            // save digilib settings in options
            data.dlOpts.birdview = settings.isBirdDivVisible ? 1 : 0;
            data.dlOpts.buttons = settings.visibleButtonSets;
            var clop = '';
            for (var o in data.dlOpts) {
                if (clop) {
                    clop += '&';
                    }
                clop += o + '=' + data.dlOpts[o];
                }
            if ($.cookie) {
                var ck = "digilib:fn:" + escape(settings.fn) + ":pn:" + settings.pn;
                console.debug("set cookie=", ck, " value=", clop);
                $.cookie(ck, clop);
                }
        }
        if (settings.interactionMode !== 'fullscreen' && $.cookie) {
            // store normal parameters in cookie for embedded mode
            var qs = getParamString(settings, settings.digilibParamNames, defaults);
            var ck = "digilib-embed:fn:" + escape(settings.fn) + ":pn:" + settings.pn;
            console.debug("set cookie=", ck, " value=", qs);
            $.cookie(ck, qs);
        }
    };

    var retrieveOptions = function (data) {
        // clop (digilib options)
        var opts = {};
        var settings = data.settings;
        if ($.cookie) {
            // read from cookie
            var ck = "digilib:fn:" + escape(settings.fn) + ":pn:" + settings.pn;
            var cp = $.cookie(ck);
            console.debug("get cookie=", ck, " value=", cp);
            // in query string format
            opts = parseQueryString(cp);
            }
        data.dlOpts = opts;
        // birdview option
        if (opts.birdview != null) {
            settings.isBirdDivVisible = opts.birdview === '1';
            }
        // visible button sets
        if (opts.buttons != null) {
            settings.visibleButtonSets = opts.buttons;
            }
    };

    // (re)load the img from a new scaler URL
    var redisplay = function (data) {
        var settings = data.settings; 
        if (settings.interactionMode === 'fullscreen') {
            // update location.href (browser URL) in fullscreen mode
            var url = getDigilibUrl(data);
            var history = window.history;
            if (typeof(history.pushState) === 'function') {
                console.debug("we could modify history, but we don't...");
                }
            window.location = url;
        } else {
            // embedded mode -- just change img src
            var url = getScalerUrl(data);
            data.$img.attr('src', url);
            // redisplay bird img
            updateBirdDiv(data);
            highlightButtons(data);
            }
    };

    // update display (overlays etc.)
    var updateDisplay = function (data) {
        updateImgTrafo(data);
        renderMarks(data);
        setupZoomDrag(data);
        if (data.settings.isBirdDivVisible) {
            renderBirdArea(data);
            setupBirdDrag(data);
        }
        // send event
        $(data).trigger('update');
    };

    // returns maximum size for scaler img in fullscreen mode
    var getFullscreenImgSize = function (data) {
        var $win = $(window);
        var winH = $win.height();
        var winW = $win.width();
        var $body = $('body');
         // include standard body margins
        var borderW = $body.outerWidth(true) - $body.width();
        var borderH = $body.outerHeight(true) - $body.height();
        // get width of first button div
        var buttonsW = 0; 
        if (data.$buttonSets) {
            buttonsW = data.$buttonSets[0].outerWidth();
        }
        // account for left/right border, body margins and additional requirements
        var calcW = winW - borderW - buttonsW - data.settings.scalerInset;
        var calcH = winH - borderH;
        console.debug(winW, winH, 'winW:', $win.width(), 'border:', borderW, 'buttonsW:', buttonsW, 'calc:', calcW);
        return geom.size(calcW, calcH);
    };

    // creates HTML structure for digilib in elem
    var setupScalerDiv = function (data) {
        var settings = data.settings;
        var $elem = data.$elem;
        $elem.addClass('digilib');
        var $img;
        var scalerUrl;
        if (settings.interactionMode === 'fullscreen') {
            // fullscreen
            $elem.addClass('dl_fullscreen');
            var imgSize = getFullscreenImgSize(data);
            // fitwidth/height omits destination height/width
            if (data.dlOpts.fitheight == null) {
                settings.dw = imgSize.width;
            }
            if (data.dlOpts.fitwidth == null) {
                settings.dh = imgSize.height;
            }
            scalerUrl = getScalerUrl(data);
            $img = $('<img/>');
        } else {
            // embedded mode -- try to keep img tag
            $elem.addClass('dl_embedded');
            scalerUrl = getScalerUrl(data);
            $img = $elem.find('img');
            if ($img.length > 0) {
                oldUrl = $img.attr('src');
                if (oldUrl === scalerUrl) {
                    console.debug("img detach:", $img);
                    $img.detach();
                } else {
                    $img = $('<img/>');
                }
            } else {
                $img = $('<img/>');
            }
        }
        // create new inner html, keep buttons
        $elem.contents(":not(div.buttons)").remove();
        var $scaler = $('<div class="scaler"/>');
        // scaler should be the first child element?
        $elem.prepend($scaler);
        $scaler.append($img);
        $img.addClass('pic');
        data.$scaler = $scaler;
        data.$img = $img;
        // setup image load handler before setting the src attribute (IE bug)
        $img.load(scalerImgLoadedHandler(data));
        $img.error(function () {console.error("error loading scaler image");});
        $img.attr('src', scalerUrl);
    };

    // 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 buttonSettings = settings.buttonSettings[mode];
        var buttonGroup = buttonSettings.buttonSets[buttonSetIdx];
        if (buttonGroup == null) {
            // no buttons here
            return;
        }
        var $buttonsDiv = $('<div class="buttons"/>');
        var buttonNames = buttonSettings[buttonGroup];
        for (var i = 0; i < buttonNames.length; i++) {
            var buttonName = buttonNames[i];
            var buttonConfig = settings.buttons[buttonName];
            // construct the button html
            var $button = $('<div class="button"></div>');
            var $a = $('<a/>');
            var $img = $('<img class="button"/>');
            $buttonsDiv.append($button);
            $button.append($a);
            $a.append($img);
            // add attributes and bindings
            $button.attr('title', buttonConfig.tooltip);
            $button.addClass('button-' + buttonName);
            // create handler for the buttons
            $a.bind('click.digilib', (function () {
                // we create a new closure to capture the value of action
                var action = buttonConfig.onclick;
                if ($.isArray(action)) {
                    // the handler function calls digilib with action and parameters
                    return function (evt) {
                        console.debug('click action=', action, ' evt=', evt);
                        $elem.digilib.apply($elem, action);
                        return false;
                    };
                } else {
                    // the handler function calls digilib with action
                    return function (evt) {
                        console.debug('click action=', action, ' evt=', evt);
                        $elem.digilib(action);
                        return false;
                    };
                }
            })());
            $img.attr('src', buttonSettings.imagePath + buttonConfig.icon);
        }
        // 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;
    };

    // creates HTML structure for the bird's eye view in elem
    var setupBirdDiv = function (data) {
        var $elem = data.$elem;
        // the bird's eye div
        var $birdDiv = $('<div class="birdview" style="display:none"/>');
        // the detail indicator frame
        var $birdZoom = $('<div class="birdzoom" style="display:none; background-color:transparent;"/>');
        // the small image
        var $birdImg = $('<img class="birdimg"/>');
        data.$birdDiv = $birdDiv;
        data.$birdZoom = $birdZoom;
        data.$birdImg = $birdImg;
        $elem.append($birdDiv);
        $birdDiv.append($birdZoom);
        $birdDiv.append($birdImg);
        // $birdZoom.css(data.settings.birdIndicatorStyle);
        var birdUrl = getBirdImgUrl(data);
        $birdImg.load(birdImgLoadedHandler(data));
        $birdImg.error(function () {console.error("error loading birdview image");});
        $birdImg.attr('src', birdUrl);
    };

    // update bird's eye view
    var updateBirdDiv = function (data) {
        if (!data.settings.isBirdDivVisible) return;
        var $birdImg = data.$birdImg;
        var oldsrc = $birdImg.attr('src');
        var newsrc = getBirdImgUrl(data);
        if (oldsrc !== newsrc) {
            $birdImg.attr('src', newsrc);
            // onload handler re-renders
        } else {
            // re-render
            renderBirdArea(data);
            // enable click and drag
            setupBirdDrag(data);
        }
    };

    // creates HTML structure for the about view in elem
    var setupAboutDiv = function (data) {
        var $elem = data.$elem;
        var settings = data.settings;
        var $aboutDiv = $('<div class="about" style="display:none"/>');
        var $header = $('<p>Digilib Graphic Viewer</p>');
        var $link = $('<a/>');
        var $logo = $('<img class="logo" title="digilib"/>');
        var $content = $('<p/>');
        $elem.append($aboutDiv);
        $aboutDiv.append($header);
        $aboutDiv.append($link);
        $aboutDiv.append($content);
        $link.append($logo);
        $logo.attr('src', settings.logoUrl);
        $link.attr('href', settings.homeUrl);
        $content.text('Version: ' + settings.version);
        data.$aboutDiv = $aboutDiv;
        // click hides
        $aboutDiv.bind('click.digilib', function () {
            actions['showAboutDiv'](data, false);
            });
    };

    // shows some window e.g. 'about' (toggle visibility if show is null)
    var showDiv = function (isVisible, $div, show) {
        if (show == null) {
            // toggle visibility
            isVisible = !isVisible;
        } else {
            // set visibility
            isVisible = show;
            }
        if (isVisible) {
            $div.fadeIn();
        } else {
            $div.fadeOut();
            }
        return isVisible;
    };

    // display more (or less) button sets
    var showButtons = function (data, more, setIdx, animated) {
        var atime = animated ? 'fast': 0;
        if (more) {
            // add set
            var $otherSets = data.$elem.find('div.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.buttons:visible');
            $otherSets.animate({right : '-='+btnWidth+'px'}, atime);
        }
        return true;
    };

    // check for buttons to highlight
    var highlightButtons = function (data, name, on) {
        var $buttons = data.$elem.find('div.buttons:visible'); // include hidden?
        // add a class for highlighted button
        var highlight = function (name, on) {
            var $button = $buttons.find('div.button-' + name);
            if (on) {
                $button.addClass('button-on');
            } else {
                $button.removeClass('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('help', settings.isAboutDivVisible);
        highlight('hmir', flags.hmir);
        highlight('vmir', flags.vmir);
        highlight('quality', flags.q1 || flags.q2);
        highlight('zoomin', ! isFullArea(data.zoomArea));
        };

    // create Transform from area and $img
    var getImgTrafo = function ($img, area, rot, hmir, vmir, mode, data) {
        var picrect = geom.rectangle($img);
        if (mode != null) {
            var imgInfo = data.imgInfo;
            if (mode === 'pixel') {
                // scaler mo=clip - image area size does not come from ww, wh
                if (imgInfo != null) {
                    area.width = picrect.width / imgInfo.width;
                    area.height = picrect.height / imgInfo.height;
                } else {
                    console.error("No image info for pixel mode!");
                }
            }
            if (mode === 'size') {
                // scaler mo=osize - image area size does not come from ww, wh
                if (imgInfo != null) {
                    var ddpi = parseFloat(data.settings.ddpi);
                    area.width = (picrect.width / ddpi) / (imgInfo.width / imgInfo.dpi_x);
                    area.height = (picrect.height / ddpi) / (imgInfo.height / imgInfo.dpi_y);
                } else {
                    console.error("No image info for original size mode!");
                }
            }
        }
        var trafo = geom.transform();
        // move zoom area offset to center
        trafo.concat(trafo.getTranslation(geom.position(-area.x, -area.y)));
        // scale zoom area size to [1,1]
        trafo.concat(trafo.getScale(geom.size(1/area.width, 1/area.height)));
        // rotate and mirror (around transformed image center i.e. [0.5,0.5])
        if (rot || hmir || vmir) {
            // move [0.5,0.5] to center
            trafo.concat(trafo.getTranslation(geom.position(-0.5, -0.5)));
            if (hmir) {
                // mirror about center
                trafo.concat(trafo.getMirror('y'));
                }
            if (vmir) {
                // mirror about center
                trafo.concat(trafo.getMirror('x'));
                }
            if (rot) {
                // rotate around center
                trafo.concat(trafo.getRotation(parseFloat(rot)));
                }
            // move back
            trafo.concat(trafo.getTranslation(geom.position(0.5, 0.5)));
            }
        // scale to screen position and size
        trafo.concat(trafo.getScale(picrect));
        trafo.concat(trafo.getTranslation(picrect));
        return trafo;
    };

    // update scaler image transform
    var updateImgTrafo = function (data) {
        var $img = data.$img;
        if ($img != null && $img.get(0).complete) {
            // create Transform from current zoomArea and image size
            data.imgTrafo = getImgTrafo($img, data.zoomArea,
                    data.settings.rot, data.scalerFlags.hmir, data.scalerFlags.vmir,
                    data.scaleMode, data);
            // console.debug("imgTrafo=", data.imgTrafo);
        }
    };

    // returns function for load event of scaler img
    var scalerImgLoadedHandler = function (data) {
        return function () {
            var $img = $(this);
            console.debug("scaler img loaded=",$img);
            var $scaler = data.$scaler;
            var imgRect = geom.rectangle($img);
            // adjust scaler div size
            imgRect.adjustDiv($scaler);
            // show image in case it was hidden (for example in zoomDrag)
            $img.css('visibility', 'visible');
            $scaler.css({'opacity' : '1', 'background-image' : 'none'});
            // update display (render marks, etc.)
            updateDisplay(data);
        };
    };

    // returns function for load event of bird's eye view img
    var birdImgLoadedHandler = function (data) {
        return function () {
            var $birdImg = $(this);
            var birdRect = geom.rectangle($birdImg);
            console.debug("birdImg loaded!", $birdImg, "rect=", birdRect, "data=", data);
            if (birdRect.width === 0) {
                // malheureusement IE7 calls load handler when there is no size info yet 
                setTimeout(function () { $birdImg.triggerHandler('load'); }, 200);
                }
            // update display (zoom area indicator)
            updateDisplay(data);
        };
    };

    // place marks on the image
    var renderMarks = function (data) {
        if (data.$img == null || data.imgTrafo == null) return;
        console.debug("rendermarks img=",data.$img," imgtrafo=",data.imgTrafo);
        var $elem = data.$elem;
        var marks = data.marks;
        // clear marks
        $elem.find('div.mark').remove();
        for (var i = 0; i < marks.length; i++) {
            var mark = marks[i];
            if (data.zoomArea.containsPosition(mark)) {
                var mpos = data.imgTrafo.transform(mark);
                console.debug("renderMarks: mpos=",mpos);
                // create mark
                var html = '<div class="mark">'+(i+1)+'</div>';
                var $mark = $(html);
                $elem.append($mark);
                mpos.adjustDiv($mark);
                }
            }
    };

    // show zoom area indicator on bird's eye view
    var renderBirdArea = function (data) {
        if (data.$birdImg == null || ! data.$birdImg.get(0).complete) return;
        var $birdZoom = data.$birdZoom;
        var zoomArea = data.zoomArea;
        var normalSize = isFullArea(zoomArea);
        if (normalSize) {
            $birdZoom.hide();
            return;
        } else {
            $birdZoom.show();
        }
        // create Transform from current area and picsize
        data.birdTrafo = getImgTrafo(data.$birdImg, FULL_AREA);
        var zoomRect = data.birdTrafo.transform(zoomArea);
        console.debug("renderBirdArea:", zoomRect, "zoomArea:", zoomArea, "$birdTrafo:", data.birdTrafo);
        // acount for border width
        var bw = getBorderWidth($birdZoom);
        zoomRect.addPosition({x : -bw, y : -bw});
        if (data.settings.interactionMode === 'fullscreen') {
            // no animation for fullscreen
            zoomRect.adjustDiv($birdZoom);
        } else {
            // nice animation for embedded mode :-)
            // correct offsetParent because animate doesn't use offset
            var ppos = $birdZoom.offsetParent().offset();
            var dest = {
                left : (zoomRect.x - ppos.left) + 'px',
                top : (zoomRect.y - ppos.top) + 'px',
                width : zoomRect.width,
                height : zoomRect.height
                };
            $birdZoom.animate(dest);
        }
    };

    // zooms by the given factor
    var zoomBy = function(data, factor) {
        var area = data.zoomArea;
        var newarea = area.copy();
        // scale
        newarea.width /= factor;
        newarea.height /= factor;
        // and recenter
        newarea.x -= 0.5 * (newarea.width - area.width);
        newarea.y -= 0.5 * (newarea.height - area.height);
        newarea = FULL_AREA.fit(newarea);
        data.zoomArea = newarea;
        redisplay(data);
    };

    // add a mark where clicked
    var setMark = function (data) {
        var $scaler = data.$scaler;
        // unbind other handler
        $scaler.unbind(".dlZoomDrag");
        // start event capturing
        $scaler.one('mousedown.dlSetMark', function (evt) {
            // event handler adding a new mark
            console.log("setmark at=", evt);
            var mpos = geom.position(evt);
            var pos = data.imgTrafo.invtransform(mpos);
            data.marks.push(pos);
            redisplay(data);
            return false;
        });
    };

    // zoom to the area around two clicked points
    var zoomArea = function(data) {
        $elem = data.$elem;
        $scaler = data.$scaler;
        var pt1, pt2;
        var $zoomDiv = $('<div class="zoomrect" style="display:none"/>');
        $elem.append($zoomDiv);
        // $zoomDiv.css(data.settings.zoomrectStyle);
        var picRect = geom.rectangle($scaler);
        // FIX ME: is there a way to query the border width from CSS info?
        // rect.x -= 2; // account for overlay borders
        // rect.y -= 2;

        var zoomStart = function (evt) {
            pt1 = geom.position(evt);
            // setup and show zoom div
            pt1.adjustDiv($zoomDiv);
            $zoomDiv.width(0).height(0);
            $zoomDiv.show();
            // register events
            $elem.bind("mousemove.dlZoomArea", zoomMove);
            $elem.bind("mouseup.dlZoomArea", zoomEnd);
            return false;
        };

        // mouse move handler
        var zoomMove = function (evt) {
            pt2 = geom.position(evt);
            var rect = geom.rectangle(pt1, pt2);
            rect.clipTo(picRect);
            // update zoom div
            rect.adjustDiv($zoomDiv);
            return false;
        };

        // mouseup handler: end moving
        var zoomEnd = function (evt) {
            pt2 = geom.position(evt);
            // assume a click and continue if the area is too small
            var clickRect = geom.rectangle(pt1, pt2);
            if (clickRect.getArea() <= 5) return false;
            // hide zoom div
            $zoomDiv.remove();
            // unregister events
            $elem.unbind("mousemove.dlZoomArea", zoomMove);
            $elem.unbind("mouseup.dlZoomArea", zoomEnd);
            // clip and transform
            clickRect.clipTo(picRect);
            var area = data.imgTrafo.invtransform(clickRect);
            data.zoomArea = area;
            // zoomed is always fit
            data.settings.ws = 1;
            delete data.dlOpts.fitwidth;
            delete data.dlOpts.fitheight;
            redisplay(data);
            return false;
        };

        // clear old handler (also ZoomDrag)
        $scaler.unbind('.dlZoomArea');
        $scaler.unbind(".dlZoomDrag");
        $elem.unbind('.dlZoomArea');
        // bind start zoom handler
        $scaler.one('mousedown.dlZoomArea', zoomStart);
    };

    // bird's eye view zoom area click and drag handler
    var setupBirdDrag = function(data) {
        var $birdImg = data.$birdImg;
        var $birdZoom = data.$birdZoom;
        var $document = $(document);
        var $scaler = data.$scaler;
        var startPos, newRect, birdImgRect, birdZoomRect, fullRect, scalerPos;
        var bw = getBorderWidth($birdZoom);

        // mousedown handler: start dragging bird zoom to a new position
        var birdZoomStartDrag = function(evt) {
            startPos = geom.position(evt);
            // position may have changed
            data.birdTrafo = getImgTrafo($birdImg, FULL_AREA);
            birdImgRect = geom.rectangle($birdImg);
            birdZoomRect = geom.rectangle($birdZoom);
            scalerPos = geom.position($scaler);
            newRect = null;
            fullRect = setZoomBG(data); // setup zoom background image
            $document.bind("mousemove.dlBirdMove", birdZoomMove);
            $document.bind("mouseup.dlBirdMove", birdZoomEndDrag);
            return false;
        };

        // mousemove handler: drag
        var birdZoomMove = function(evt) {
            var pos = geom.position(evt);
            var delta = startPos.delta(pos);
            // move birdZoom div, keeping size
            newRect = birdZoomRect.copy();
            newRect.addPosition(delta);
            newRect.stayInside(birdImgRect);
            // reflect birdview zoom position in scaler image
            var area = data.birdTrafo.invtransform(newRect);
            var imgArea = data.imgTrafo.transform(area);
            var offset = imgArea.getPosition().neg();
            offset.add(scalerPos);
            if (fullRect) {
                var bgPos = fullRect.getPosition().add(offset);
            } else {
                var bgPos = offset;
            }
            // move the background image to the new position
            data.$scaler.css({
                'background-position' : bgPos.x + "px " + bgPos.y + "px"
                });
            // acount for border width
            newRect.addPosition({x : -bw, y : -bw});
            newRect.adjustDiv($birdZoom);
            return false;
        };

        // mouseup handler: reload page
        var birdZoomEndDrag = function(evt) {
            var settings = data.settings;
            $document.unbind("mousemove.dlBirdMove", birdZoomMove);
            $document.unbind("mouseup.dlBirdMove", birdZoomEndDrag);
            if (newRect == null) { 
                // no movement happened - set center to click position
                startPos = birdZoomRect.getCenter();
                birdZoomMove(evt); 
                }
            // ugly, but needed to prevent double border width compensation
            newRect.addPosition({x : bw, y : bw});
            var newArea = data.birdTrafo.invtransform(newRect);
            data.zoomArea = newArea;
            redisplay(data);
            return false;
        };

        // clear old handler
        $document.unbind(".dlBirdMove");
        $birdImg.unbind(".dlBirdMove");
        $birdZoom.unbind(".dlBirdMove");
        if (! isFullArea(data.zoomArea)) {
            // set new handler
            $birdImg.bind("mousedown.dlBirdMove", birdZoomStartDrag);
            $birdZoom.bind("mousedown.dlBirdMove", birdZoomStartDrag);
        }
    };

    // move bird zoom indicator to reflect zoomed detail area
    var setBirdZoom = function(data, rect) {
        var part = data.imgTrafo.invtransform(rect);
        // area = FULL_AREA.fit(part); // no, we want to see where we transcend the borders
        birdTrafo = getImgTrafo(data.$birdImg, FULL_AREA);
        var birdRect = birdTrafo.transform(part);
        var $birdZoom = data.$birdZoom;
        // acount for border width
        var bw = getBorderWidth($birdZoom);
        birdRect.addPosition({x : -bw, y : -bw});
        birdRect.adjustDiv(data.$birdZoom);
    };

    // set zoom background
    var setZoomBG = function(data) {
        var $scaler = data.$scaler;
        var $img = data.$img;
        var fullRect = null;
        // hide marks
        data.$elem.find('div.mark').hide();
        // hide the scaler img, show background of div instead
        $img.css('visibility', 'hidden');
        var scalerCss = {
            'background-image' : 'url(' + $img.attr('src') + ')',
            'background-repeat' : 'no-repeat',
            'background-position' : 'left top',
            'opacity' : '0.5',
            'cursor' : 'move'
            };
        if (data.hasBgSize) {
            // full-size background using CSS3-background-size
            fullRect = data.imgTrafo.transform(FULL_AREA);
            if (fullRect.height < data.settings.maxBgSize && fullRect.width < data.settings.maxBgSize) {
                // correct offset because background is relative
                var scalerPos = geom.position($scaler);
                fullRect.addPosition(scalerPos.neg());
                var url = getBirdImgUrl(data, ['rot', 'mo']);
                scalerCss['background-image'] = 'url(' + url + ')';
                scalerCss[data.bgSizeName] = fullRect.width + 'px ' + fullRect.height + 'px';
                scalerCss['background-position'] = fullRect.x + 'px '+ fullRect.y + 'px';
            } else {
                // too big
                fullRect = null;
                }
            }
            $scaler.css(scalerCss);
            // isBgReady = true;
        return fullRect;
    };

    // setup handlers for dragging the zoomed image
    var setupZoomDrag = function(data) {
        var startPos, delta, fullRect;
        var $document = $(document);
        var $elem = data.$elem;
        var $scaler = data.$scaler;
        var $img = data.$img;

        // drag the image and load a new detail on mouse up
        var dragStart = function (evt) {
            console.debug("dragstart at=",evt);
            // don't start dragging if not zoomed
            if (isFullArea(data.zoomArea)) return false;
            startPos = geom.position(evt);
            delta = null;
            // set low res background immediately on mousedown
            fullRect = setZoomBG(data);
            $document.bind("mousemove.dlZoomDrag", dragMove);
            $document.bind("mouseup.dlZoomDrag", dragEnd);
            return false;
            };

        // mousemove handler: drag zoomed image
        var dragMove = function (evt) {
            var pos = geom.position(evt);
            delta = startPos.delta(pos);
            if (fullRect) {
                var bgPos = fullRect.getPosition().add(delta);
            } else {
                var bgPos = delta;
            }
            // move the background image to the new position
            $scaler.css({
                'background-position' : bgPos.x + "px " + bgPos.y + "px"
                });
            // set birdview indicator to reflect new zoom position
            var za = geom.rectangle($img);
            za.addPosition(delta.neg());
            setBirdZoom(data, za);
            return false;
            };

        // mouseup handler: reload zoomed image in new position
        var dragEnd = function (evt) {
            $scaler.css('cursor', 'auto');
            $document.unbind("mousemove.dlZoomDrag", dragMove);
            $document.unbind("mouseup.dlZoomDrag", dragEnd);
            if (delta == null || delta.distance() < 2) {
                // no movement
                $img.css('visibility', 'visible');
                $scaler.css({'opacity' : '1', 'background-image' : 'none'});
                // unhide marks
                data.$elem.find('div.mark').show();
                return false; 
            }
            // get old zoom area (screen coordinates)
            var za = geom.rectangle($img);
            // move
            za.addPosition(delta.neg());
            // transform back
            var newArea = data.imgTrafo.invtransform(za);
            data.zoomArea = FULL_AREA.fit(newArea);
            redisplay(data);
            return false;
            };

        // clear old handler
        $document.unbind(".dlZoomDrag");
        $scaler.unbind(".dlZoomDrag");
        if (! isFullArea(data.zoomArea)) {
            // set handler
            $scaler.bind("mousedown.dlZoomDrag", dragStart);
        }
    };

    // get image quality as a number (0..2)
    var getQuality = function (data) {
        var flags = data.scalerFlags;
        var q = flags.q2 || flags.q1 || 'q0'; // assume q0 as default
        return parseInt(q[1], 10);
    };

    // set image quality as a number (0..2)
    var setQuality = function (data, qual) {
        var flags = data.scalerFlags;
        // clear flags
        for (var i = 0; i < 3; ++i) {
            delete flags['q'+i];
            }
        flags['q'+qual] = 'q'+qual;
    };

    // get image scale mode (screen, pixel, size)
    var getScaleMode = function (data) {
        if (data.scalerFlags.clip != null) {
            return 'pixel';
        } else if (data.scalerFlags.osize != null) {
            return 'size';
        }
        // mo=fit is default
        return 'screen';
    };

    // set image scale mode (screen, pixel, size)
    var setScaleMode = function (data, mode) {
        delete data.scalerFlags.fit;
        delete data.scalerFlags.clip;
        delete data.scalerFlags.osize;
        if (mode === 'pixel') {
            data.scalerFlags.clip = 'clip';
        } else if (mode === 'size') {
            data.scalerFlags.osize = 'osize';
        }
        // mo=fit is default
    };

     // 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];
    };

    // auxiliary function, assuming equal border width on all sides
    var getBorderWidth = function($elem) {
        var border = $elem.outerWidth() - $elem.width();
        return border/2;
    };

    // auxiliary function (from old dllib.js)
    var isFullArea = function(area) {
        return (area.width === 1.0) && (area.height === 1.0);
    };

    // auxiliary function (from Douglas Crockford, A.10)
    var isNumber = function isNumber(value) {
        return typeof value === 'number' && isFinite(value);
    };

    // auxiliary function to crop senseless precision
    var cropFloat = function (x) {
        return parseInt(10000 * x, 10) / 10000;
    };

    // fallback for console.log calls
    if (customConsole) {
        var logFunction = function(type) {
            return function(){
                var $debug = $('#debug'); // debug div
                if (!$debug) return;
                var args = Array.prototype.slice.call(arguments);
                var argtext = args.join(' ');
                var $logDiv = $('<div/>');
                $logDiv.addClass(type);
                $logDiv.text(argtext);
                $debug.append($logDiv);
                };
            };
        console.log = logFunction('_log'); 
        console.debug = logFunction('_debug'); 
        console.error = logFunction('_error');
        }

    // hook plugin into jquery
    $.fn.digilib = function (action) {
        // plugin extension mechanism
        if (action === 'plugin') {
            var plugin = arguments[1];
            // each plugin needs a name
            if (plugin.name != null) {
                plugins[plugin.name] = plugin;
            }
            /* for each digilib $elem extend data.settings with obj.options
            // TODO: couldn't other plugins just access $elem.data('digilib')?
            if (obj.options) {
                this.each(function() {
                    var $elem = $(this);
                    // console.debug('extending:', $elem);
                    var data = $elem.data('digilib');
                    if (!data) {
                        return console.log('cannot extend digilib plugin, element not initialised!');
                        }
                    var settings = data.settings;
                    $.extend(settings, obj.options);
                    // console.log('settings:', settings);
                    });
                delete(obj.options);
                }
            // extend the plugin actions (to make this useful, 
            // maybe we need to expose some more internal functions)
            $.extend(actions, obj); */
        } else if (actions[action]) {
            // call action on this with the remaining arguments (inserting data as first argument)
            var $elem = $(this);
            var data = $elem.data('digilib');
            var args = Array.prototype.slice.call(arguments, 1);
            args.unshift(data);
            return actions[action].apply(this, args);
        } else if (typeof(action) === 'object' || !action) {
            // call init on this
            return actions.init.apply(this, arguments);
        } else {
            $.error('action ' + action + ' does not exist on jQuery.digilib');
        }
    };

})(jQuery);