// Copyright 2008, 2009 Hannes Hochreiner
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU 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 Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see http://www.gnu.org/licenses/.

// Set onload event handler.
window.onload = jessyInkInit;

// Creating a namespace dictionary. The standard Inkscape namespaces are taken from inkex.py.
var NSS = new Object();
NSS['sodipodi']='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd';
NSS['cc']='http://web.resource.org/cc/';
NSS['svg']='http://www.w3.org/2000/svg';
NSS['dc']='http://purl.org/dc/elements/1.1/';
NSS['rdf']='http://www.w3.org/1999/02/22-rdf-syntax-ns#';
NSS['inkscape']='http://www.inkscape.org/namespaces/inkscape';
NSS['xlink']='http://www.w3.org/1999/xlink';
NSS['xml']='http://www.w3.org/XML/1998/namespace';
NSS['jessyink']='https://launchpad.net/jessyink';

// Keycodes.
var LEFT_KEY = 37; // cursor left keycode
var UP_KEY = 38; // cursor up keycode
var RIGHT_KEY = 39; // cursor right keycode
var DOWN_KEY = 40; // cursor down keycode
var PAGE_UP_KEY = 33; // page up keycode
var PAGE_DOWN_KEY = 34; // page down keycode
var HOME_KEY = 36; // home keycode
var END_KEY = 35; // end keycode
var ENTER_KEY = 13; // next slide
var SPACE_KEY = 32;
var ESCAPE_KEY = 27;

// Presentation modes.
var SLIDE_MODE = 1;
var INDEX_MODE = 2;
var DRAWING_MODE = 3;

// Mouse handler actions.
var MOUSE_UP = 1;
var MOUSE_DOWN = 2;
var MOUSE_MOVE = 3;
var MOUSE_WHEEL = 4;

// Parameters.
var ROOT_NODE = document.getElementsByTagNameNS(NSS["svg"], "svg")[0];
var HEIGHT = 0;
var WIDTH = 0;
var INDEX_COLUMNS_DEFAULT = 4;
var INDEX_COLUMNS = INDEX_COLUMNS_DEFAULT;
var INDEX_OFFSET = 0;
var STATE_START = -1;
var STATE_END = -2;
var BACKGROUND_COLOR = null;
var slides = new Array();

// Initialisation.
var currentMode = SLIDE_MODE;
var masterSlide = null;
var activeSlide = 0;
var activeEffect = 0;
var timeStep = 30; // 40 ms equal 25 frames per second.
var lastFrameTime = null;
var processingEffect = false;
var transCounter = 0;
var effectArray = 0;
var defaultTransitionInDict = new Object();
defaultTransitionInDict["name"] = "appear";
var defaultTransitionOutDict = new Object();
defaultTransitionOutDict["name"] = "appear";
var jessyInkInitialised = false;

// Initialise char and key code dictionaries.
var charCodeDictionary = getDefaultCharCodeDictionary();
var keyCodeDictionary = getDefaultKeyCodeDictionary();

// Initialise mouse handler dictionary.
var mouseHandlerDictionary = getDefaultMouseHandlerDictionary();

var progress_bar_visible = false;
var timer_elapsed = 0;
var timer_start = timer_elapsed;
var timer_duration = 15; // 15 minutes

var history_counter = 0;
var history_original_elements = new Array();
var history_presentation_elements = new Array();

var mouse_original_path = null;
var mouse_presentation_path = null;
var mouse_last_x = -1;
var mouse_last_y = -1;
var mouse_min_dist_sqr = 3 * 3;
var path_colour = "red";
var path_width_default = 3;
var path_width = path_width_default;
var path_paint_width = path_width;

var number_of_added_slides = 0;

/** Initialisation function.
 *  The whole presentation is set-up in this function.
 */
function jessyInkInit()
{
    // Make sure we only execute this code once. Double execution can occur if the onload event handler is set
    // in the main svg tag as well (as was recommended in earlier versions). Executing this function twice does
    // not lead to any problems, but it takes more time.
    if (jessyInkInitialised)
        return;

    // Making the presentation scalable.
    var VIEWBOX = ROOT_NODE.getAttribute("viewBox");

    if (VIEWBOX)
    {
        WIDTH = ROOT_NODE.viewBox.animVal.width;
        HEIGHT = ROOT_NODE.viewBox.animVal.height;
    }
    else
    {
        HEIGHT = parseFloat(ROOT_NODE.getAttribute("height"));
        WIDTH = parseFloat(ROOT_NODE.getAttribute("width"));
        ROOT_NODE.setAttribute("viewBox", "0 0 " + WIDTH + " " + HEIGHT);
    }

    ROOT_NODE.setAttribute("width", "100%");
    ROOT_NODE.setAttribute("height", "100%");

    // Setting the background color.
    var namedViews = document.getElementsByTagNameNS(NSS["sodipodi"], "namedview");

    for (var counter = 0; counter < namedViews.length; counter++)
    {
        if (namedViews[counter].hasAttribute("id") && namedViews[counter].hasAttribute("pagecolor"))
        {
            if (namedViews[counter].getAttribute("id") == "base")
            {
                BACKGROUND_COLOR = namedViews[counter].getAttribute("pagecolor");
                var newAttribute = "background-color:" + BACKGROUND_COLOR + ";";

                if (ROOT_NODE.hasAttribute("style"))
                    newAttribute += ROOT_NODE.getAttribute("style");

                ROOT_NODE.setAttribute("style", newAttribute);
            }
        }
    }

    // Defining clip-path.
    var defsNodes = document.getElementsByTagNameNS(NSS["svg"], "defs");

    if (defsNodes.length > 0)
    {
        var existingClipPath = document.getElementById("jessyInkSlideClipPath");

        if (!existingClipPath)
        {
            var rectNode = document.createElementNS(NSS["svg"], "rect");
            var clipPath = document.createElementNS(NSS["svg"], "clipPath");

            rectNode.setAttribute("x", 0);
            rectNode.setAttribute("y", 0);
            rectNode.setAttribute("width", WIDTH);
            rectNode.setAttribute("height", HEIGHT);

            clipPath.setAttribute("id", "jessyInkSlideClipPath");
            clipPath.setAttribute("clipPathUnits", "userSpaceOnUse");

            clipPath.appendChild(rectNode);
            defsNodes[0].appendChild(clipPath);
        }
    }

    // Making a list of the slide and finding the master slide.
    var nodes = document.getElementsByTagNameNS(NSS["svg"], "g");
    var tempSlides = new Array();
    var existingJessyInkPresentationLayer = null;

    for (var counter = 0; counter < nodes.length; counter++)
    {
        if (nodes[counter].getAttributeNS(NSS["inkscape"], "groupmode") && (nodes[counter].getAttributeNS(NSS["inkscape"], "groupmode") == "layer"))
        {
            if (nodes[counter].getAttributeNS(NSS["inkscape"], "label") && nodes[counter].getAttributeNS(NSS["jessyink"], "masterSlide") == "masterSlide")
                masterSlide = nodes[counter];
            else if (nodes[counter].getAttributeNS(NSS["inkscape"], "label") && nodes[counter].getAttributeNS(NSS["jessyink"], "presentationLayer") == "presentationLayer")
                existingJessyInkPresentationLayer = nodes[counter];
            else
                tempSlides.push(nodes[counter].getAttribute("id"));
        }
        else if (nodes[counter].getAttributeNS(NSS['jessyink'], 'element'))
        {
            handleElement(nodes[counter]);
        }
    }

    // Hide master slide set default transitions.
    if (masterSlide)
    {
        masterSlide.style.display = "none";

        if (masterSlide.hasAttributeNS(NSS["jessyink"], "transitionIn"))
            defaultTransitionInDict = propStrToDict(masterSlide.getAttributeNS(NSS["jessyink"], "transitionIn"));

        if (masterSlide.hasAttributeNS(NSS["jessyink"], "transitionOut"))
            defaultTransitionOutDict = propStrToDict(masterSlide.getAttributeNS(NSS["jessyink"], "transitionOut"));
    }

    if (existingJessyInkPresentationLayer != null)
    {
        existingJessyInkPresentationLayer.parentNode.removeChild(existingJessyInkPresentationLayer);
    }

    // Set start slide.
    var hashObj = new LocationHash(window.location.hash);

    activeSlide = hashObj.slideNumber;
    activeEffect = hashObj.effectNumber;

    if (activeSlide < 0)
        activeSlide = 0;
    else if (activeSlide >= tempSlides.length)
        activeSlide = tempSlides.length - 1;

    var originalNode = document.getElementById(tempSlides[counter]);

    var JessyInkPresentationLayer = document.createElementNS(NSS["svg"], "g");
    JessyInkPresentationLayer.setAttributeNS(NSS["inkscape"], "groupmode", "layer");
    JessyInkPresentationLayer.setAttributeNS(NSS["inkscape"], "label", "JessyInk Presentation Layer");
    JessyInkPresentationLayer.setAttributeNS(NSS["jessyink"], "presentationLayer", "presentationLayer");
    JessyInkPresentationLayer.setAttribute("id", "jessyink_presentation_layer");
    JessyInkPresentationLayer.style.display = "inherit";
    ROOT_NODE.appendChild(JessyInkPresentationLayer);

    // Gathering all the information about the transitions and effects of the slides, set the background
    // from the master slide and substitute the auto-texts.
    for (var counter = 0; counter < tempSlides.length; counter++)
    {
        var originalNode = document.getElementById(tempSlides[counter]);
        originalNode.style.display = "none";
        var node = suffixNodeIds(originalNode.cloneNode(true), "_" + counter);
        JessyInkPresentationLayer.appendChild(node);
        slides[counter] = new Object();
        slides[counter]["original_element"] = originalNode;
        slides[counter]["element"] = node;

        // Set build in transition.
        slides[counter]["transitionIn"] = new Object();

        var dict;

        if (node.hasAttributeNS(NSS["jessyink"], "transitionIn"))
            dict = propStrToDict(node.getAttributeNS(NSS["jessyink"], "transitionIn"));
        else
            dict = defaultTransitionInDict;

        slides[counter]["transitionIn"]["name"] = dict["name"];
        slides[counter]["transitionIn"]["options"] = new Object();

        for (key in dict)
            if (key != "name")
                slides[counter]["transitionIn"]["options"][key] = dict[key];

        // Set build out transition.
        slides[counter]["transitionOut"] = new Object();

        if (node.hasAttributeNS(NSS["jessyink"], "transitionOut"))
            dict = propStrToDict(node.getAttributeNS(NSS["jessyink"], "transitionOut"));
        else
            dict = defaultTransitionOutDict;

        slides[counter]["transitionOut"]["name"] = dict["name"];
        slides[counter]["transitionOut"]["options"] = new Object();

        for (key in dict)
            if (key != "name")
                slides[counter]["transitionOut"]["options"][key] = dict[key];

        // Copy master slide content.
        if (masterSlide)
        {
            var clonedNode = suffixNodeIds(masterSlide.cloneNode(true), "_" + counter);
            clonedNode.removeAttributeNS(NSS["inkscape"], "groupmode");
            clonedNode.removeAttributeNS(NSS["inkscape"], "label");
            clonedNode.style.display = "inherit";

            node.insertBefore(clonedNode, node.firstChild);
        }

        // Setting clip path.
        node.setAttribute("clip-path", "url(#jessyInkSlideClipPath)");

        // Substitute auto texts.
        substituteAutoTexts(node, node.getAttributeNS(NSS["inkscape"], "label"), counter + 1, tempSlides.length);

        node.removeAttributeNS(NSS["inkscape"], "groupmode");
        node.removeAttributeNS(NSS["inkscape"], "label");

        // Set effects.
        var tempEffects = new Array();
        var groups = new Object();

        for (var IOCounter = 0; IOCounter <= 1; IOCounter++)
        {
            var propName = "";
            var dir = 0;

            if (IOCounter == 0)
            {
                propName = "effectIn";
                dir = 1;
            }
            else if (IOCounter == 1)
            {
                propName = "effectOut";
                dir = -1;
            }

            var effects = getElementsByPropertyNS(node, NSS["jessyink"], propName);

            for (var effectCounter = 0; effectCounter < effects.length; effectCounter++)
            {
                var element = document.getElementById(effects[effectCounter]);
                var dict = propStrToDict(element.getAttributeNS(NSS["jessyink"], propName));

                // Put every element that has an effect associated with it, into its own group.
                // Unless of course, we already put it into its own group.
                if (!(groups[element.id]))
                {
                    var newGroup = document.createElementNS(NSS["svg"], "g");

                    element.parentNode.insertBefore(newGroup, element);
                    newGroup.appendChild(element.parentNode.removeChild(element));
                    groups[element.id] = newGroup;
                }

                var effectDict = new Object();

                effectDict["effect"] = dict["name"];
                effectDict["dir"] = dir;
                effectDict["element"] = groups[element.id];

                for (var option in dict)
                {
                    if ((option != "name") && (option != "order"))
                    {
                        if (!effectDict["options"])
                            effectDict["options"] = new Object();

                        effectDict["options"][option] = dict[option];
                    }
                }

                if (!tempEffects[dict["order"]])
                    tempEffects[dict["order"]] = new Array();

                tempEffects[dict["order"]][tempEffects[dict["order"]].length] = effectDict;
            }
        }

        // Make invisible, but keep in rendering tree to ensure that bounding box can be calculated.
        node.setAttribute("opacity",0);
        node.style.display = "inherit";

        // Create a transform group.
        var transformGroup = document.createElementNS(NSS["svg"], "g");

        // Add content to transform group.
        while (node.firstChild)
            transformGroup.appendChild(node.firstChild);

        // Transfer the transform attribute from the node to the transform group.
        if (node.getAttribute("transform"))
        {
            transformGroup.setAttribute("transform", node.getAttribute("transform"));
            node.removeAttribute("transform");
        }

        // Create a view group.
        var viewGroup = document.createElementNS(NSS["svg"], "g");

        viewGroup.appendChild(transformGroup);
        slides[counter]["viewGroup"] = node.appendChild(viewGroup);

        // Insert background.
        if (BACKGROUND_COLOR != null)
        {
            var rectNode = document.createElementNS(NSS["svg"], "rect");

            rectNode.setAttribute("x", 0);
            rectNode.setAttribute("y", 0);
            rectNode.setAttribute("width", WIDTH);
            rectNode.setAttribute("height", HEIGHT);
            rectNode.setAttribute("id", "jessyInkBackground" + counter);
            rectNode.setAttribute("fill", BACKGROUND_COLOR);

            slides[counter]["viewGroup"].insertBefore(rectNode, slides[counter]["viewGroup"].firstChild);
        }

        // Set views.
        var tempViews = new Array();
        var views = getElementsByPropertyNS(node, NSS["jessyink"], "view");
        var matrixOld = (new matrixSVG()).fromElements(1, 0, 0, 0, 1, 0, 0, 0, 1);

        // Set initial view even if there are no other views.
        slides[counter]["viewGroup"].setAttribute("transform", matrixOld.toAttribute());
        slides[counter].initialView = matrixOld.toAttribute();

        for (var viewCounter = 0; viewCounter < views.length; viewCounter++)
        {
            var element = document.getElementById(views[viewCounter]);
            var dict = propStrToDict(element.getAttributeNS(NSS["jessyink"], "view"));

            if (dict["order"] == 0)
            {
                matrixOld = pointMatrixToTransformation(rectToMatrix(element)).mult((new matrixSVG()).fromSVGMatrix(slides[counter].viewGroup.getScreenCTM()).inv().mult((new matrixSVG()).fromSVGMatrix(element.parentNode.getScreenCTM())).inv());
                slides[counter].initialView = matrixOld.toAttribute();
            }
            else
            {
                var effectDict = new Object();

                effectDict["effect"] = dict["name"];
                effectDict["dir"] = 1;
                effectDict["element"] = slides[counter]["viewGroup"];
                effectDict["order"] = dict["order"];

                for (var option in dict)
                {
                    if ((option != "name") && (option != "order"))
                    {
                        if (!effectDict["options"])
                            effectDict["options"] = new Object();

                        effectDict["options"][option] = dict[option];
                    }
                }

                effectDict["options"]["matrixNew"] = pointMatrixToTransformation(rectToMatrix(element)).mult((new matrixSVG()).fromSVGMatrix(slides[counter].viewGroup.getScreenCTM()).inv().mult((new matrixSVG()).fromSVGMatrix(element.parentNode.getScreenCTM())).inv());

                tempViews[dict["order"]] = effectDict;
            }

            // Remove element.
            element.parentNode.removeChild(element);
        }

        // Consolidate view array and append it to the effect array.
        if (tempViews.length > 0)
        {
            for (var viewCounter = 0; viewCounter < tempViews.length; viewCounter++)
            {
                if (tempViews[viewCounter])
                {
                    tempViews[viewCounter]["options"]["matrixOld"] = matrixOld;
                    matrixOld = tempViews[viewCounter]["options"]["matrixNew"];

                    if (!tempEffects[tempViews[viewCounter]["order"]])
                        tempEffects[tempViews[viewCounter]["order"]] = new Array();

                    tempEffects[tempViews[viewCounter]["order"]][tempEffects[tempViews[viewCounter]["order"]].length] = tempViews[viewCounter];
                }
            }
        }

        // Set consolidated effect array.
        if (tempEffects.length > 0)
        {
            slides[counter]["effects"] = new Array();

            for (var effectCounter = 0; effectCounter < tempEffects.length; effectCounter++)
            {
                if (tempEffects[effectCounter])
                    slides[counter]["effects"][slides[counter]["effects"].length] = tempEffects[effectCounter];
            }
        }

        node.setAttribute("onmouseover", "if ((currentMode == INDEX_MODE) && ( activeSlide != " + counter + ")) { indexSetActiveSlide(" + counter + "); };");

        // Set visibility for initial state.
        if (counter == activeSlide)
        {
            node.style.display = "inherit";
            node.setAttribute("opacity",1);
        }
        else
        {
            node.style.display = "none";
            node.setAttribute("opacity",0);
        }
    }

    // Set key handler.
    var jessyInkObjects = document.getElementsByTagNameNS(NSS["svg"], "g");

    for (var counter = 0; counter < jessyInkObjects.length; counter++)
    {
        var elem = jessyInkObjects[counter];

        if (elem.getAttributeNS(NSS["jessyink"], "customKeyBindings"))
        {
            if (elem.getCustomKeyBindings != undefined)
                keyCodeDictionary = elem.getCustomKeyBindings();

            if (elem.getCustomCharBindings != undefined)
                charCodeDictionary = elem.getCustomCharBindings();
        }
    }

    // Set mouse handler.
    var jessyInkMouseHandler = document.getElementsByTagNameNS(NSS["jessyink"], "mousehandler");

    for (var counter = 0; counter < jessyInkMouseHandler.length; counter++)
    {
        var elem = jessyInkMouseHandler[counter];

        if (elem.getMouseHandler != undefined)
        {
            var tempDict = elem.getMouseHandler();

            for (mode in tempDict)
            {
                if (!mouseHandlerDictionary[mode])
                    mouseHandlerDictionary[mode] = new Object();

                for (handler in tempDict[mode])
                    mouseHandlerDictionary[mode][handler] = tempDict[mode][handler];
            }
        }
    }

    // Check effect number.
    if ((activeEffect < 0) || (!slides[activeSlide].effects))
    {
        activeEffect = 0;
    }
    else if (activeEffect > slides[activeSlide].effects.length)
    {
        activeEffect = slides[activeSlide].effects.length;
    }

    createProgressBar(JessyInkPresentationLayer);
    hideProgressBar();
    setProgressBarValue(activeSlide);
    setTimeIndicatorValue(0);
    setInterval("updateTimer()", 1000);
    setSlideToState(activeSlide, activeEffect);
    jessyInkInitialised = true;
}

/** Function to substitute the auto-texts.
 *
 *  @param node the node
 *  @param slideName name of the slide the node is on
 *  @param slideNumber number of the slide the node is on
 *  @param numberOfSlides number of slides in the presentation
 */
function substituteAutoTexts(node, slideName, slideNumber, numberOfSlides)
{
    var texts = node.getElementsByTagNameNS(NSS["svg"], "tspan");

    for (var textCounter = 0; textCounter < texts.length; textCounter++)
    {
        if (texts[textCounter].getAttributeNS(NSS["jessyink"], "autoText") == "slideNumber")
            texts[textCounter].firstChild.nodeValue = slideNumber;
        else if (texts[textCounter].getAttributeNS(NSS["jessyink"], "autoText") == "numberOfSlides")
            texts[textCounter].firstChild.nodeValue = numberOfSlides;
        else if (texts[textCounter].getAttributeNS(NSS["jessyink"], "autoText") == "slideTitle")
            texts[textCounter].firstChild.nodeValue = slideName;
    }
}

/** Convenience function to get an element depending on whether it has a property with a particular name.
 *    This function emulates some dearly missed XPath functionality.
 *
 *  @param node the node
 *  @param namespace namespace of the attribute
 *  @param name attribute name
 */
function getElementsByPropertyNS(node, namespace, name)
{
    var elems = new Array();

    if (node.getAttributeNS(namespace, name))
        elems.push(node.getAttribute("id"));

    for (var counter = 0; counter < node.childNodes.length; counter++)
    {
        if (node.childNodes[counter].nodeType == 1)
            elems = elems.concat(getElementsByPropertyNS(node.childNodes[counter], namespace, name));
    }

    return elems;
}

/** Function to dispatch the next effect, if there is none left, change the slide.
 *
 *  @param dir direction of the change (1 = forwards, -1 = backwards)
 */
function dispatchEffects(dir)
{
    if (slides[activeSlide]["effects"] && (((dir == 1) && (activeEffect < slides[activeSlide]["effects"].length)) || ((dir == -1) && (activeEffect > 0))))
    {
        processingEffect = true;

        if (dir == 1)
        {
            effectArray = slides[activeSlide]["effects"][activeEffect];
            activeEffect += dir;
        }
        else if (dir == -1)
        {
            activeEffect += dir;
            effectArray = slides[activeSlide]["effects"][activeEffect];
        }

        transCounter = 0;
        startTime = (new Date()).getTime();
        lastFrameTime = null;
        effect(dir);
    }
    else if (((dir == 1) && (activeSlide < (slides.length - 1))) || (((dir == -1) && (activeSlide > 0))))
    {
        changeSlide(dir);
    }
}

/** Function to skip effects and directly either put the slide into start or end state or change slides.
 *
 *  @param dir direction of the change (1 = forwards, -1 = backwards)
 */
function skipEffects(dir)
{
    if (slides[activeSlide]["effects"] && (((dir == 1) && (activeEffect < slides[activeSlide]["effects"].length)) || ((dir == -1) && (activeEffect > 0))))
    {
        processingEffect = true;

        if (slides[activeSlide]["effects"] && (dir == 1))
            activeEffect = slides[activeSlide]["effects"].length;
        else
            activeEffect = 0;

        if (dir == 1)
            setSlideToState(activeSlide, STATE_END);
        else
            setSlideToState(activeSlide, STATE_START);

        processingEffect = false;
    }
    else if (((dir == 1) && (activeSlide < (slides.length - 1))) || (((dir == -1) && (activeSlide > 0))))
    {
        changeSlide(dir);
    }
}

/** Function to change between slides.
 *
 *  @param dir direction (1 = forwards, -1 = backwards)
 */
function changeSlide(dir)
{
    processingEffect = true;
    effectArray = new Array();

    effectArray[0] = new Object();
    if (dir == 1)
    {
        effectArray[0]["effect"] = slides[activeSlide]["transitionOut"]["name"];
        effectArray[0]["options"] = slides[activeSlide]["transitionOut"]["options"];
        effectArray[0]["dir"] = -1;
    }
    else if (dir == -1)
    {
        effectArray[0]["effect"] = slides[activeSlide]["transitionIn"]["name"];
        effectArray[0]["options"] = slides[activeSlide]["transitionIn"]["options"];
        effectArray[0]["dir"] = 1;
    }
    effectArray[0]["element"] = slides[activeSlide]["element"];

    activeSlide += dir;
    setProgressBarValue(activeSlide);

    effectArray[1] = new Object();

    if (dir == 1)
    {
        effectArray[1]["effect"] = slides[activeSlide]["transitionIn"]["name"];
        effectArray[1]["options"] = slides[activeSlide]["transitionIn"]["options"];
        effectArray[1]["dir"] = 1;
    }
    else if (dir == -1)
    {
        effectArray[1]["effect"] = slides[activeSlide]["transitionOut"]["name"];
        effectArray[1]["options"] = slides[activeSlide]["transitionOut"]["options"];
        effectArray[1]["dir"] = -1;
    }

    effectArray[1]["element"] = slides[activeSlide]["element"];

    if (slides[activeSlide]["effects"] && (dir == -1))
        activeEffect = slides[activeSlide]["effects"].length;
    else
        activeEffect = 0;

    if (dir == -1)
        setSlideToState(activeSlide, STATE_END);
    else
        setSlideToState(activeSlide, STATE_START);

    transCounter = 0;
    startTime = (new Date()).getTime();
    lastFrameTime = null;
    effect(dir);
}

/** Function to toggle between index and slide mode.
*/
function toggleSlideIndex()
{
    var suspendHandle = ROOT_NODE.suspendRedraw(500);

    if (currentMode == SLIDE_MODE)
    {
        hideProgressBar();
        INDEX_OFFSET = -1;
        indexSetPageSlide(activeSlide);
        currentMode = INDEX_MODE;
    }
    else if (currentMode == INDEX_MODE)
    {
        for (var counter = 0; counter < slides.length; counter++)
        {
            slides[counter]["element"].setAttribute("transform","scale(1)");

            if (counter == activeSlide)
            {
                slides[counter]["element"].style.display = "inherit";
                slides[counter]["element"].setAttribute("opacity",1);
                activeEffect = 0;
            }
            else
            {
                slides[counter]["element"].setAttribute("opacity",0);
                slides[counter]["element"].style.display = "none";
            }
        }
        currentMode = SLIDE_MODE;
        setSlideToState(activeSlide, STATE_START);
        setProgressBarValue(activeSlide);

        if (progress_bar_visible)
        {
            showProgressBar();
        }
    }

    ROOT_NODE.unsuspendRedraw(suspendHandle);
    ROOT_NODE.forceRedraw();
}

/** Function to run an effect.
 *
 *  @param dir direction in which to play the effect (1 = forwards, -1 = backwards)
 */
function effect(dir)
{
    var done = true;

    var suspendHandle = ROOT_NODE.suspendRedraw(200);

    for (var counter = 0; counter < effectArray.length; counter++)
    {
        if (effectArray[counter]["effect"] == "fade")
            done &= fade(parseInt(effectArray[counter]["dir"]) * dir, effectArray[counter]["element"], transCounter, effectArray[counter]["options"]);
        else if (effectArray[counter]["effect"] == "appear")
            done &= appear(parseInt(effectArray[counter]["dir"]) * dir, effectArray[counter]["element"], transCounter, effectArray[counter]["options"]);
        else if (effectArray[counter]["effect"] == "pop")
            done &= pop(parseInt(effectArray[counter]["dir"]) * dir, effectArray[counter]["element"], transCounter, effectArray[counter]["options"]);
        else if (effectArray[counter]["effect"] == "view")
            done &= view(parseInt(effectArray[counter]["dir"]) * dir, effectArray[counter]["element"], transCounter, effectArray[counter]["options"]);
    }

    ROOT_NODE.unsuspendRedraw(suspendHandle);
    ROOT_NODE.forceRedraw();

    if (!done)
    {
        var currentTime = (new Date()).getTime();
        var timeDiff = 1;

        transCounter = currentTime - startTime;

        if (lastFrameTime != null)
        {
            timeDiff = timeStep - (currentTime - lastFrameTime);

            if (timeDiff <= 0)
                timeDiff = 1;
        }

        lastFrameTime = currentTime;

        window.setTimeout("effect(" + dir + ")", timeDiff);
    }
    else
    {
        window.location.hash = (activeSlide + 1) + '_' + activeEffect;
        processingEffect = false;
    }
}

/** Function to display the index sheet.
 *
 *  @param offsetNumber offset number
 */
function displayIndex(offsetNumber)
{
    var offsetX = 0;
    var offsetY = 0;

    if (offsetNumber < 0)
        offsetNumber = 0;
    else if (offsetNumber >= slides.length)
        offsetNumber = slides.length - 1;

    for (var counter = 0; counter < slides.length; counter++)
    {
        if ((counter < offsetNumber) || (counter > offsetNumber + INDEX_COLUMNS * INDEX_COLUMNS - 1))
        {
            slides[counter]["element"].setAttribute("opacity",0);
            slides[counter]["element"].style.display = "none";
        }
        else
        {
            offsetX = ((counter - offsetNumber) % INDEX_COLUMNS) * WIDTH;
            offsetY = Math.floor((counter - offsetNumber) / INDEX_COLUMNS) * HEIGHT;

            slides[counter]["element"].setAttribute("transform","scale("+1/INDEX_COLUMNS+") translate("+offsetX+","+offsetY+")");
            slides[counter]["element"].style.display = "inherit";
            slides[counter]["element"].setAttribute("opacity",0.5);
        }

        setSlideToState(counter, STATE_END);
    }

    //do we need to save the current offset?
    if (INDEX_OFFSET != offsetNumber)
        INDEX_OFFSET = offsetNumber;
}

/** Function to set the active slide in the slide view.
 *
 *  @param nbr index of the active slide
 */
function slideSetActiveSlide(nbr)
{
    if (nbr >= slides.length)
        nbr = slides.length - 1;
    else if (nbr < 0)
        nbr = 0;

    slides[activeSlide]["element"].setAttribute("opacity",0);
    slides[activeSlide]["element"].style.display = "none";

    activeSlide = parseInt(nbr);

    setSlideToState(activeSlide, STATE_START);
    slides[activeSlide]["element"].style.display = "inherit";
    slides[activeSlide]["element"].setAttribute("opacity",1);

    activeEffect = 0;
    setProgressBarValue(nbr);
}

/** Function to set the active slide in the index view.
 *
 *  @param nbr index of the active slide
 */
function indexSetActiveSlide(nbr)
{
    if (nbr >= slides.length)
        nbr = slides.length - 1;
    else if (nbr < 0)
        nbr = 0;

    slides[activeSlide]["element"].setAttribute("opacity",0.5);

    activeSlide = parseInt(nbr);
    window.location.hash = (activeSlide + 1) + '_0';

    slides[activeSlide]["element"].setAttribute("opacity",1);
}

/** Function to set the page and active slide in index view.
 *
 *  @param nbr index of the active slide
 *
 *  NOTE: To force a redraw,
 *  set INDEX_OFFSET to -1 before calling indexSetPageSlide().
 *
 *  This is necessary for zooming (otherwise the index might not
 *  get redrawn) and when switching to index mode.
 *
 *  INDEX_OFFSET = -1
 *  indexSetPageSlide(activeSlide);
 */
function indexSetPageSlide(nbr)
{
    if (nbr >= slides.length)
        nbr = slides.length - 1;
    else if (nbr < 0)
        nbr = 0;

    //calculate the offset
    var offset = nbr - nbr % (INDEX_COLUMNS * INDEX_COLUMNS);

    if (offset < 0)
        offset = 0;

    //if different from kept offset, then record and change the page
    if (offset != INDEX_OFFSET)
    {
        INDEX_OFFSET = offset;
        displayIndex(INDEX_OFFSET);
    }

    //set the active slide
    indexSetActiveSlide(nbr);
}

/** Event handler for key press.
 *
 *  @param e the event
 */
function keydown(e)
{
    if (!e)
        e = window.event;

    code = e.keyCode || e.charCode;

    if (!processingEffect && keyCodeDictionary[currentMode] && keyCodeDictionary[currentMode][code])
        return keyCodeDictionary[currentMode][code]();
    else
        document.onkeypress = keypress;
}
// Set event handler for key down.
document.onkeydown = keydown;

/** Event handler for key press.
 *
 *  @param e the event
 */
function keypress(e)
{
    document.onkeypress = null;

    if (!e)
        e = window.event;

    str = String.fromCharCode(e.keyCode || e.charCode);

    if (!processingEffect && charCodeDictionary[currentMode] && charCodeDictionary[currentMode][str])
        return charCodeDictionary[currentMode][str]();
}

/** Function to supply the default char code dictionary.
 *
 * @returns default char code dictionary
 */
function getDefaultCharCodeDictionary()
{
    var charCodeDict = new Object();

    charCodeDict[SLIDE_MODE] = new Object();
    charCodeDict[INDEX_MODE] = new Object();
    charCodeDict[DRAWING_MODE] = new Object();

    charCodeDict[SLIDE_MODE]["i"] = function () { return toggleSlideIndex(); };
    charCodeDict[SLIDE_MODE]["d"] = function () { return slideSwitchToDrawingMode(); };
    charCodeDict[SLIDE_MODE]["D"] = function () { return slideQueryDuration(); };
    charCodeDict[SLIDE_MODE]["n"] = function () { return slideAddSlide(activeSlide); };
    charCodeDict[SLIDE_MODE]["p"] = function () { return slideToggleProgressBarVisibility(); };
    charCodeDict[SLIDE_MODE]["t"] = function () { return slideResetTimer(); };
    charCodeDict[SLIDE_MODE]["e"] = function () { return slideUpdateExportLayer(); };

    charCodeDict[DRAWING_MODE]["d"] = function () { return drawingSwitchToSlideMode(); };
    charCodeDict[DRAWING_MODE]["0"] = function () { return drawingResetPathWidth(); };
    charCodeDict[DRAWING_MODE]["1"] = function () { return drawingSetPathWidth(1.0); };
    charCodeDict[DRAWING_MODE]["3"] = function () { return drawingSetPathWidth(3.0); };
    charCodeDict[DRAWING_MODE]["5"] = function () { return drawingSetPathWidth(5.0); };
    charCodeDict[DRAWING_MODE]["7"] = function () { return drawingSetPathWidth(7.0); };
    charCodeDict[DRAWING_MODE]["9"] = function () { return drawingSetPathWidth(9.0); };
    charCodeDict[DRAWING_MODE]["b"] = function () { return drawingSetPathColour("blue"); };
    charCodeDict[DRAWING_MODE]["c"] = function () { return drawingSetPathColour("cyan"); };
    charCodeDict[DRAWING_MODE]["g"] = function () { return drawingSetPathColour("green"); };
    charCodeDict[DRAWING_MODE]["k"] = function () { return drawingSetPathColour("black"); };
    charCodeDict[DRAWING_MODE]["m"] = function () { return drawingSetPathColour("magenta"); };
    charCodeDict[DRAWING_MODE]["o"] = function () { return drawingSetPathColour("orange"); };
    charCodeDict[DRAWING_MODE]["r"] = function () { return drawingSetPathColour("red"); };
    charCodeDict[DRAWING_MODE]["w"] = function () { return drawingSetPathColour("white"); };
    charCodeDict[DRAWING_MODE]["y"] = function () { return drawingSetPathColour("yellow"); };
    charCodeDict[DRAWING_MODE]["z"] = function () { return drawingUndo(); };

    charCodeDict[INDEX_MODE]["i"] = function () { return toggleSlideIndex(); };
    charCodeDict[INDEX_MODE]["-"] = function () { return indexDecreaseNumberOfColumns(); };
    charCodeDict[INDEX_MODE]["="] = function () { return indexIncreaseNumberOfColumns(); };
    charCodeDict[INDEX_MODE]["+"] = function () { return indexIncreaseNumberOfColumns(); };
    charCodeDict[INDEX_MODE]["0"] = function () { return indexResetNumberOfColumns(); };

    return charCodeDict;
}

/** Function to supply the default key code dictionary.
 *
 * @returns default key code dictionary
 */
function getDefaultKeyCodeDictionary()
{
    var keyCodeDict = new Object();

    keyCodeDict[SLIDE_MODE] = new Object();
    keyCodeDict[INDEX_MODE] = new Object();
    keyCodeDict[DRAWING_MODE] = new Object();

    keyCodeDict[SLIDE_MODE][LEFT_KEY] = function() { return dispatchEffects(-1); };
    keyCodeDict[SLIDE_MODE][RIGHT_KEY] = function() { return dispatchEffects(1); };
    keyCodeDict[SLIDE_MODE][UP_KEY] = function() { return skipEffects(-1); };
    keyCodeDict[SLIDE_MODE][DOWN_KEY] = function() { return skipEffects(1); };
    keyCodeDict[SLIDE_MODE][PAGE_UP_KEY] = function() { return dispatchEffects(-1); };
    keyCodeDict[SLIDE_MODE][PAGE_DOWN_KEY] = function() { return dispatchEffects(1); };
    keyCodeDict[SLIDE_MODE][HOME_KEY] = function() { return slideSetActiveSlide(0); };
    keyCodeDict[SLIDE_MODE][END_KEY] = function() { return slideSetActiveSlide(slides.length - 1); };
    keyCodeDict[SLIDE_MODE][SPACE_KEY] = function() { return dispatchEffects(1); };

    keyCodeDict[INDEX_MODE][LEFT_KEY] = function() { return indexSetPageSlide(activeSlide - 1); };
    keyCodeDict[INDEX_MODE][RIGHT_KEY] = function() { return indexSetPageSlide(activeSlide + 1); };
    keyCodeDict[INDEX_MODE][UP_KEY] = function() { return indexSetPageSlide(activeSlide - INDEX_COLUMNS); };
    keyCodeDict[INDEX_MODE][DOWN_KEY] = function() { return indexSetPageSlide(activeSlide + INDEX_COLUMNS); };
    keyCodeDict[INDEX_MODE][PAGE_UP_KEY] = function() { return indexSetPageSlide(activeSlide - INDEX_COLUMNS * INDEX_COLUMNS); };
    keyCodeDict[INDEX_MODE][PAGE_DOWN_KEY] = function() { return indexSetPageSlide(activeSlide + INDEX_COLUMNS * INDEX_COLUMNS); };
    keyCodeDict[INDEX_MODE][HOME_KEY] = function() { return indexSetPageSlide(0); };
    keyCodeDict[INDEX_MODE][END_KEY] = function() { return indexSetPageSlide(slides.length - 1); };
    keyCodeDict[INDEX_MODE][ENTER_KEY] = function() { return toggleSlideIndex(); };

    keyCodeDict[DRAWING_MODE][ESCAPE_KEY] = function () { return drawingSwitchToSlideMode(); };

    return keyCodeDict;
}

/** Function to handle all mouse events.
 *
 *    @param    evnt    event
 *    @param    action    type of event (e.g. mouse up, mouse wheel)
 */
function mouseHandlerDispatch(evnt, action)
{
    if (!evnt)
        evnt = window.event;

    var retVal = true;

    if (!processingEffect && mouseHandlerDictionary[currentMode] && mouseHandlerDictionary[currentMode][action])
    {
        var subRetVal = mouseHandlerDictionary[currentMode][action](evnt);

        if (subRetVal != null && subRetVal != undefined)
            retVal = subRetVal;
    }

    if (evnt.preventDefault && !retVal)
        evnt.preventDefault();

    evnt.returnValue = retVal;

    return retVal;
}

// Set mouse event handler.
document.onmousedown = function(e) { return mouseHandlerDispatch(e, MOUSE_DOWN); };
document.onmouseup = function(e) { return mouseHandlerDispatch(e, MOUSE_UP); };
document.onmousemove = function(e) { return mouseHandlerDispatch(e, MOUSE_MOVE); };

// Moz
if (window.addEventListener)
{
    window.addEventListener('DOMMouseScroll', function(e) { return mouseHandlerDispatch(e, MOUSE_WHEEL); }, false);
}

// Opera Safari OK - may not work in IE
window.onmousewheel = function(e) { return mouseHandlerDispatch(e, MOUSE_WHEEL); };

/** Function to supply the default mouse handler dictionary.
 *
 * @returns default mouse handler dictionary
 */
function getDefaultMouseHandlerDictionary()
{
    var mouseHandlerDict = new Object();

    mouseHandlerDict[SLIDE_MODE] = new Object();
    mouseHandlerDict[INDEX_MODE] = new Object();
    mouseHandlerDict[DRAWING_MODE] = new Object();

    mouseHandlerDict[SLIDE_MODE][MOUSE_DOWN] = function(evnt) { return dispatchEffects(1); };
    mouseHandlerDict[SLIDE_MODE][MOUSE_WHEEL] = function(evnt) { return slideMousewheel(evnt); };

    mouseHandlerDict[INDEX_MODE][MOUSE_DOWN] = function(evnt) { return toggleSlideIndex(); };

    mouseHandlerDict[DRAWING_MODE][MOUSE_DOWN] = function(evnt) { return drawingMousedown(evnt); };
    mouseHandlerDict[DRAWING_MODE][MOUSE_UP] = function(evnt) { return drawingMouseup(evnt); };
    mouseHandlerDict[DRAWING_MODE][MOUSE_MOVE] = function(evnt) { return drawingMousemove(evnt); };

    return mouseHandlerDict;
}

/** Function to switch from slide mode to drawing mode.
*/
function slideSwitchToDrawingMode()
{
    currentMode = DRAWING_MODE;

    var tempDict;

    if (ROOT_NODE.hasAttribute("style"))
        tempDict = propStrToDict(ROOT_NODE.getAttribute("style"));
    else
        tempDict = new Object();

    tempDict["cursor"] = "crosshair";
    ROOT_NODE.setAttribute("style", dictToPropStr(tempDict));
}

/** Function to switch from drawing mode to slide mode.
*/
function drawingSwitchToSlideMode()
{
    currentMode = SLIDE_MODE;

    var tempDict;

    if (ROOT_NODE.hasAttribute("style"))
        tempDict = propStrToDict(ROOT_NODE.getAttribute("style"));
    else
        tempDict = new Object();

    tempDict["cursor"] = "auto";
    ROOT_NODE.setAttribute("style", dictToPropStr(tempDict));
}

/** Function to decrease the number of columns in index mode.
*/
function indexDecreaseNumberOfColumns()
{
    if (INDEX_COLUMNS >= 3)
    {
        INDEX_COLUMNS -= 1;
        INDEX_OFFSET = -1
            indexSetPageSlide(activeSlide);
    }
}

/** Function to increase the number of columns in index mode.
*/
function indexIncreaseNumberOfColumns()
{
    if (INDEX_COLUMNS < 7)
    {
        INDEX_COLUMNS += 1;
        INDEX_OFFSET = -1
            indexSetPageSlide(activeSlide);
    }
}

/** Function to reset the number of columns in index mode.
*/
function indexResetNumberOfColumns()
{
    if (INDEX_COLUMNS != INDEX_COLUMNS_DEFAULT)
    {
        INDEX_COLUMNS = INDEX_COLUMNS_DEFAULT;
        INDEX_OFFSET = -1
            indexSetPageSlide(activeSlide);
    }
}

/** Function to reset path width in drawing mode.
*/
function drawingResetPathWidth()
{
    path_width = path_width_default;
    set_path_paint_width();
}

/** Function to set path width in drawing mode.
 *
 * @param width new path width
 */
function drawingSetPathWidth(width)
{
    path_width = width;
    set_path_paint_width();
}

/** Function to set path colour in drawing mode.
 *
 * @param colour new path colour
 */
function drawingSetPathColour(colour)
{
    path_colour = colour;
}

/** Function to query the duration of the presentation from the user in slide mode.
*/
function slideQueryDuration()
{
    var new_duration = prompt("Length of presentation in minutes?", timer_duration);

    if ((new_duration != null) && (new_duration != ''))
    {
        timer_duration = new_duration;
    }

    updateTimer();
}

/** Function to add new slide in slide mode.
 *
 * @param afterSlide after which slide to insert the new one
 */
function slideAddSlide(afterSlide)
{
    addSlide(afterSlide);
    slideSetActiveSlide(afterSlide + 1);
    updateTimer();
}

/** Function to toggle the visibility of the progress bar in slide mode.
*/
function slideToggleProgressBarVisibility()
{
    if (progress_bar_visible)
    {
        progress_bar_visible = false;
        hideProgressBar();
    }
    else
    {
        progress_bar_visible = true;
        showProgressBar();
    }
}

/** Function to reset the timer in slide mode.
*/
function slideResetTimer()
{
    timer_start = timer_elapsed;
    updateTimer();
}

/** Convenience function to pad a string with zero in front up to a certain length.
 */
function padString(str, len)
{
    var outStr = str;

    while (outStr.length < len)
    {
        outStr = '0' + outStr;
    }

    return outStr;
}

/** Function to update the export layer.
 */
function slideUpdateExportLayer()
{
    // Suspend redraw since we are going to mess with the slides.
    var suspendHandle = ROOT_NODE.suspendRedraw(2000);

    var tmpActiveSlide = activeSlide;
    var tmpActiveEffect = activeEffect;
    var exportedLayers = new Array();

    for (var counterSlides = 0; counterSlides < slides.length; counterSlides++)
    {
        var exportNode;

        setSlideToState(counterSlides, STATE_START);

        var maxEffect = 0;

        if (slides[counterSlides].effects)
        {
            maxEffect = slides[counterSlides].effects.length;
        }

        exportNode = slides[counterSlides].element.cloneNode(true);
        exportNode.setAttributeNS(NSS["inkscape"], "groupmode", "layer");
        exportNode.setAttributeNS(NSS["inkscape"], "label", "slide_" + padString((counterSlides + 1).toString(), slides.length.toString().length) + "_effect_" + padString("0", maxEffect.toString().length));

        exportedLayers.push(exportNode);

        if (slides[counterSlides]["effects"])
        {
            for (var counter = 0; counter < slides[counterSlides]["effects"].length; counter++)
            {
                for (var subCounter = 0; subCounter < slides[counterSlides]["effects"][counter].length; subCounter++)
                {
                    var effect = slides[counterSlides]["effects"][counter][subCounter];
                    if (effect["effect"] == "fade")
                        fade(parseInt(effect["dir"]), effect["element"], STATE_END, effect["options"]);
                    else if (effect["effect"] == "appear")
                        appear(parseInt(effect["dir"]), effect["element"], STATE_END, effect["options"]);
                    else if (effect["effect"] == "pop")
                        pop(parseInt(effect["dir"]), effect["element"], STATE_END, effect["options"]);
                    else if (effect["effect"] == "view")
                        view(parseInt(effect["dir"]), effect["element"], STATE_END, effect["options"]);
                }

                var layerName = "slide_" + padString((counterSlides + 1).toString(), slides.length.toString().length) + "_effect_" + padString((counter + 1).toString(), maxEffect.toString().length);
                exportNode = slides[counterSlides].element.cloneNode(true);
                exportNode.setAttributeNS(NSS["inkscape"], "groupmode", "layer");
                exportNode.setAttributeNS(NSS["inkscape"], "label", layerName);
                exportNode.setAttribute("id", layerName);

                exportedLayers.push(exportNode);
            }
        }
    }

    activeSlide = tmpActiveSlide;
    activeEffect = tmpActiveEffect;
    setSlideToState(activeSlide, activeEffect);

    // Copy image.
    var newDoc = document.documentElement.cloneNode(true);

    // Delete viewbox form new imag and set width and height.
    newDoc.removeAttribute('viewbox');
    newDoc.setAttribute('width', WIDTH);
    newDoc.setAttribute('height', HEIGHT);

    // Delete all layers and script elements.
    var nodesToBeRemoved = new Array();

    for (var childCounter = 0; childCounter <  newDoc.childNodes.length; childCounter++)
    {
        var child = newDoc.childNodes[childCounter];

        if (child.nodeType == 1)
        {
            if ((child.nodeName.toUpperCase() == 'G') || (child.nodeName.toUpperCase() == 'SCRIPT'))
            {
                nodesToBeRemoved.push(child);
            }
        }
    }

    for (var ndCounter = 0; ndCounter < nodesToBeRemoved.length; ndCounter++)
    {
        var nd = nodesToBeRemoved[ndCounter];

        // Before removing the node, check whether it contains any definitions.
        var defs = nd.getElementsByTagNameNS(NSS["svg"], "defs");

        for (var defsCounter = 0; defsCounter < defs.length; defsCounter++)
        {
            if (defs[defsCounter].id)
            {
                newDoc.appendChild(defs[defsCounter].cloneNode(true));
            }
        }

        // Remove node.
        nd.parentNode.removeChild(nd);
    }

    // Set current layer.
    if (exportedLayers[0])
    {
        var namedView;

        for (var nodeCounter = 0; nodeCounter < newDoc.childNodes.length; nodeCounter++)
        {
            if ((newDoc.childNodes[nodeCounter].nodeType == 1) && (newDoc.childNodes[nodeCounter].getAttribute('id') == 'base'))
            {
                namedView = newDoc.childNodes[nodeCounter];
            }
        }

        if (namedView)
        {
            namedView.setAttributeNS(NSS['inkscape'], 'current-layer', exportedLayers[0].getAttributeNS(NSS['inkscape'], 'label'));
        }
    }

    // Add exported layers.
    while (exportedLayers.length > 0)
    {
        var nd = exportedLayers.pop();

        nd.setAttribute("opacity",1);
        nd.style.display = "inherit";

        newDoc.appendChild(nd);
    }

    // Serialise the new document.
  window.location = 'data:application/svg+xml;base64;charset=utf-8,' + window.btoa(unescape(encodeURIComponent((new XMLSerializer()).serializeToString(newDoc))));

    // Unsuspend redraw.
    ROOT_NODE.unsuspendRedraw(suspendHandle);
    ROOT_NODE.forceRedraw();
}

/** Function to undo last drawing operation.
*/
function drawingUndo()
{
    mouse_presentation_path = null;
    mouse_original_path = null;

    if (history_presentation_elements.length > 0)
    {
        var p = history_presentation_elements.pop();
        var parent = p.parentNode.removeChild(p);

        p = history_original_elements.pop();
        parent = p.parentNode.removeChild(p);
    }
}

/** Event handler for mouse down in drawing mode.
 *
 *  @param e the event
 */
function drawingMousedown(e)
{
    var value = 0;

    if (e.button)
        value = e.button;
    else if (e.which)
        value = e.which;

    if (value == 1)
    {
        history_counter++;

        var p = calcCoord(e);

        mouse_last_x = e.clientX;
        mouse_last_y = e.clientY;
        mouse_original_path = document.createElementNS(NSS["svg"], "path");
        mouse_original_path.setAttribute("stroke", path_colour);
        mouse_original_path.setAttribute("stroke-width", path_paint_width);
        mouse_original_path.setAttribute("fill", "none");
        mouse_original_path.setAttribute("id", "path " + Date());
        mouse_original_path.setAttribute("d", "M" + p.x + "," + p.y);
        slides[activeSlide]["original_element"].appendChild(mouse_original_path);
        history_original_elements.push(mouse_original_path);

        mouse_presentation_path = document.createElementNS(NSS["svg"], "path");
        mouse_presentation_path.setAttribute("stroke", path_colour);
        mouse_presentation_path.setAttribute("stroke-width", path_paint_width);
        mouse_presentation_path.setAttribute("fill", "none");
        mouse_presentation_path.setAttribute("id", "path " + Date() + " presentation copy");
        mouse_presentation_path.setAttribute("d", "M" + p.x + "," + p.y);

        if (slides[activeSlide]["viewGroup"])
            slides[activeSlide]["viewGroup"].appendChild(mouse_presentation_path);
        else
            slides[activeSlide]["element"].appendChild(mouse_presentation_path);

        history_presentation_elements.push(mouse_presentation_path);

        return false;
    }

    return true;
}

/** Event handler for mouse up in drawing mode.
 *
 *  @param e the event
 */
function drawingMouseup(e)
{
    if(!e)
        e = window.event;

    if (mouse_presentation_path != null)
    {
        var p = calcCoord(e);
        var d = mouse_presentation_path.getAttribute("d");
        d += " L" + p.x + "," + p.y;
        mouse_presentation_path.setAttribute("d", d);
        mouse_presentation_path = null;
        mouse_original_path.setAttribute("d", d);
        mouse_original_path = null;

        return false;
    }

    return true;
}

/** Event handler for mouse move in drawing mode.
 *
 *  @param e the event
 */
function drawingMousemove(e)
{
    if(!e)
        e = window.event;

    var dist = (mouse_last_x - e.clientX) * (mouse_last_x - e.clientX) + (mouse_last_y - e.clientY) * (mouse_last_y - e.clientY);

    if (mouse_presentation_path == null)
    {
        return true;
    }

    if (dist >= mouse_min_dist_sqr)
    {
        var p = calcCoord(e);
        var d = mouse_presentation_path.getAttribute("d");
        d += " L" + p.x + "," + p.y;
        mouse_presentation_path.setAttribute("d", d);
        mouse_original_path.setAttribute("d", d);
        mouse_last_x = e.clientX;
        mouse_last_y = e.clientY;
    }

    return false;
}

/** Event handler for mouse wheel events in slide mode.
 *  based on http://adomas.org/javascript-mouse-wheel/
 *
 *  @param e the event
 */
function slideMousewheel(e)
{
    var delta = 0;

    if (!e)
        e = window.event;

    if (e.wheelDelta)
    { // IE Opera
        delta = e.wheelDelta/120;
    }
    else if (e.detail)
    { // MOZ
        delta = -e.detail/3;
    }

    if (delta > 0)
        skipEffects(-1);
    else if (delta < 0)
        skipEffects(1);

    if (e.preventDefault)
        e.preventDefault();

    e.returnValue = false;
}

/** Event handler for mouse wheel events in index mode.
 *  based on http://adomas.org/javascript-mouse-wheel/
 *
 *  @param e the event
 */
function indexMousewheel(e)
{
    var delta = 0;

    if (!e)
        e = window.event;

    if (e.wheelDelta)
    { // IE Opera
        delta = e.wheelDelta/120;
    }
    else if (e.detail)
    { // MOZ
        delta = -e.detail/3;
    }

    if (delta > 0)
        indexSetPageSlide(activeSlide - INDEX_COLUMNS * INDEX_COLUMNS);
    else if (delta < 0)
        indexSetPageSlide(activeSlide + INDEX_COLUMNS * INDEX_COLUMNS);

    if (e.preventDefault)
        e.preventDefault();

    e.returnValue = false;
}

/** Function to set the path paint width.
*/
function set_path_paint_width()
{
    var svgPoint1 = document.documentElement.createSVGPoint();
    var svgPoint2 = document.documentElement.createSVGPoint();

    svgPoint1.x = 0.0;
    svgPoint1.y = 0.0;
    svgPoint2.x = 1.0;
    svgPoint2.y = 0.0;

    var matrix = slides[activeSlide]["element"].getTransformToElement(ROOT_NODE);

    if (slides[activeSlide]["viewGroup"])
        matrix = slides[activeSlide]["viewGroup"].getTransformToElement(ROOT_NODE);

    svgPoint1 = svgPoint1.matrixTransform(matrix);
    svgPoint2 = svgPoint2.matrixTransform(matrix);

    path_paint_width = path_width / Math.sqrt((svgPoint2.x - svgPoint1.x) * (svgPoint2.x - svgPoint1.x) + (svgPoint2.y - svgPoint1.y) * (svgPoint2.y - svgPoint1.y));
}

/** The view effect.
 *
 *  @param dir direction the effect should be played (1 = forwards, -1 = backwards)
 *  @param element the element the effect should be applied to
 *  @param time the time that has elapsed since the beginning of the effect
 *  @param options a dictionary with additional options (e.g. length of the effect); for the view effect the options need to contain the old and the new matrix.
 */
function view(dir, element, time, options)
{
    var length = 250;
    var fraction;

    if (!options["matrixInitial"])
    {
        var tempString = slides[activeSlide]["viewGroup"].getAttribute("transform");

        if (tempString)
            options["matrixInitial"] = (new matrixSVG()).fromAttribute(tempString);
        else
            options["matrixInitial"] = (new matrixSVG()).fromSVGElements(1, 0, 0, 1, 0, 0);
    }

    if ((time == STATE_END) || (time == STATE_START))
        fraction = 1;
    else
    {
        if (options && options["length"])
            length = options["length"];

        fraction = time / length;
    }

    if (dir == 1)
    {
        if (fraction <= 0)
        {
            element.setAttribute("transform", options["matrixInitial"].toAttribute());
        }
        else if (fraction >= 1)
        {
            element.setAttribute("transform", options["matrixNew"].toAttribute());

            set_path_paint_width();

            options["matrixInitial"] = null;
            return true;
        }
        else
        {
            element.setAttribute("transform", options["matrixInitial"].mix(options["matrixNew"], fraction).toAttribute());
        }
    }
    else if (dir == -1)
    {
        if (fraction <= 0)
        {
            element.setAttribute("transform", options["matrixInitial"].toAttribute());
        }
        else if (fraction >= 1)
        {
            element.setAttribute("transform", options["matrixOld"].toAttribute());
            set_path_paint_width();

            options["matrixInitial"] = null;
            return true;
        }
        else
        {
            element.setAttribute("transform", options["matrixInitial"].mix(options["matrixOld"], fraction).toAttribute());
        }
    }

    return false;
}

/** The fade effect.
 *
 *  @param dir direction the effect should be played (1 = forwards, -1 = backwards)
 *  @param element the element the effect should be applied to
 *  @param time the time that has elapsed since the beginning of the effect
 *  @param options a dictionary with additional options (e.g. length of the effect)
 */
function fade(dir, element, time, options)
{
    var length = 250;
    var fraction;

    if ((time == STATE_END) || (time == STATE_START))
        fraction = 1;
    else
    {
        if (options && options["length"])
            length = options["length"];

        fraction = time / length;
    }

    if (dir == 1)
    {
        if (fraction <= 0)
        {
            element.style.display = "none";
            element.setAttribute("opacity", 0);
        }
        else if (fraction >= 1)
        {
            element.style.display = "inherit";
            element.setAttribute("opacity", 1);
            return true;
        }
        else
        {
            element.style.display = "inherit";
            element.setAttribute("opacity", fraction);
        }
    }
    else if (dir == -1)
    {
        if (fraction <= 0)
        {
            element.style.display = "inherit";
            element.setAttribute("opacity", 1);
        }
        else if (fraction >= 1)
        {
            element.setAttribute("opacity", 0);
            element.style.display = "none";
            return true;
        }
        else
        {
            element.style.display = "inherit";
            element.setAttribute("opacity", 1 - fraction);
        }
    }
    return false;
}

/** The appear effect.
 *
 *  @param dir direction the effect should be played (1 = forwards, -1 = backwards)
 *  @param element the element the effect should be applied to
 *  @param time the time that has elapsed since the beginning of the effect
 *  @param options a dictionary with additional options (e.g. length of the effect)
 */
function appear(dir, element, time, options)
{
    if (dir == 1)
    {
        element.style.display = "inherit";
        element.setAttribute("opacity",1);
    }
    else if (dir == -1)
    {
        element.style.display = "none";
        element.setAttribute("opacity",0);
    }
    return true;
}

/** The pop effect.
 *
 *  @param dir direction the effect should be played (1 = forwards, -1 = backwards)
 *  @param element the element the effect should be applied to
 *  @param time the time that has elapsed since the beginning of the effect
 *  @param options a dictionary with additional options (e.g. length of the effect)
 */
function pop(dir, element, time, options)
{
    var length = 500;
    var fraction;

    if ((time == STATE_END) || (time == STATE_START))
        fraction = 1;
    else
    {
        if (options && options["length"])
            length = options["length"];

        fraction = time / length;
    }

    if (dir == 1)
    {
        if (fraction <= 0)
        {
            element.setAttribute("opacity", 0);
            element.setAttribute("transform", "scale(0)");
            element.style.display = "none";
        }
        else if (fraction >= 1)
        {
            element.setAttribute("opacity", 1);
            element.removeAttribute("transform");
            element.style.display = "inherit";
            return true;
        }
        else
        {
            element.style.display = "inherit";
            var opacityFraction = fraction * 3;
            if (opacityFraction > 1)
                opacityFraction = 1;
            element.setAttribute("opacity", opacityFraction);
            var offsetX = WIDTH * (1.0 - fraction) / 2.0;
            var offsetY = HEIGHT * (1.0 - fraction) / 2.0;
            element.setAttribute("transform", "translate(" + offsetX + "," + offsetY + ") scale(" + fraction + ")");
        }
    }
    else if (dir == -1)
    {
        if (fraction <= 0)
        {
            element.setAttribute("opacity", 1);
            element.setAttribute("transform", "scale(1)");
            element.style.display = "inherit";
        }
        else if (fraction >= 1)
        {
            element.setAttribute("opacity", 0);
            element.removeAttribute("transform");
            element.style.display = "none";
            return true;
        }
        else
        {
            element.setAttribute("opacity", 1 - fraction);
            element.setAttribute("transform", "scale(" + 1 - fraction + ")");
            element.style.display = "inherit";
        }
    }
    return false;
}

/** Function to set a slide either to the start or the end state.
 *
 *  @param slide the slide to use
 *  @param state the state into which the slide should be set
 */
function setSlideToState(slide, state)
{
    slides[slide]["viewGroup"].setAttribute("transform", slides[slide].initialView);

    if (slides[slide]["effects"])
    {
        if (state == STATE_END)
        {
            for (var counter = 0; counter < slides[slide]["effects"].length; counter++)
            {
                for (var subCounter = 0; subCounter < slides[slide]["effects"][counter].length; subCounter++)
                {
                    var effect = slides[slide]["effects"][counter][subCounter];
                    if (effect["effect"] == "fade")
                        fade(effect["dir"], effect["element"], STATE_END, effect["options"]);
                    else if (effect["effect"] == "appear")
                        appear(effect["dir"], effect["element"], STATE_END, effect["options"]);
                    else if (effect["effect"] == "pop")
                        pop(effect["dir"], effect["element"], STATE_END, effect["options"]);
                    else if (effect["effect"] == "view")
                        view(effect["dir"], effect["element"], STATE_END, effect["options"]);
                }
            }
        }
        else if (state == STATE_START)
        {
            for (var counter = slides[slide]["effects"].length - 1; counter >= 0; counter--)
            {
                for (var subCounter = 0; subCounter < slides[slide]["effects"][counter].length; subCounter++)
                {
                    var effect = slides[slide]["effects"][counter][subCounter];
                    if (effect["effect"] == "fade")
                        fade(parseInt(effect["dir"]) * -1, effect["element"], STATE_START, effect["options"]);
                    else if (effect["effect"] == "appear")
                        appear(parseInt(effect["dir"]) * -1, effect["element"], STATE_START, effect["options"]);
                    else if (effect["effect"] == "pop")
                        pop(parseInt(effect["dir"]) * -1, effect["element"], STATE_START, effect["options"]);
                    else if (effect["effect"] == "view")
                        view(parseInt(effect["dir"]) * -1, effect["element"], STATE_START, effect["options"]);
                }
            }
        }
        else
        {
            setSlideToState(slide, STATE_START);

            for (var counter = 0; counter < slides[slide]["effects"].length && counter < state; counter++)
            {
                for (var subCounter = 0; subCounter < slides[slide]["effects"][counter].length; subCounter++)
                {
                    var effect = slides[slide]["effects"][counter][subCounter];
                    if (effect["effect"] == "fade")
                        fade(effect["dir"], effect["element"], STATE_END, effect["options"]);
                    else if (effect["effect"] == "appear")
                        appear(effect["dir"], effect["element"], STATE_END, effect["options"]);
                    else if (effect["effect"] == "pop")
                        pop(effect["dir"], effect["element"], STATE_END, effect["options"]);
                    else if (effect["effect"] == "view")
                        view(effect["dir"], effect["element"], STATE_END, effect["options"]);
                }
            }
        }
    }

    window.location.hash = (activeSlide + 1) + '_' + activeEffect;
}

/** Convenience function to translate a attribute string into a dictionary.
 *
 *    @param str the attribute string
 *  @return a dictionary
 *  @see dictToPropStr
 */
function propStrToDict(str)
{
    var list = str.split(";");
    var obj = new Object();

    for (var counter = 0; counter < list.length; counter++)
    {
        var subStr = list[counter];
        var subList = subStr.split(":");
        if (subList.length == 2)
        {
            obj[subList[0]] = subList[1];
        }
    }

    return obj;
}

/** Convenience function to translate a dictionary into a string that can be used as an attribute.
 *
 *  @param dict the dictionary to convert
 *  @return a string that can be used as an attribute
 *  @see propStrToDict
 */
function dictToPropStr(dict)
{
    var str = "";

    for (var key in dict)
    {
        str += key + ":" + dict[key] + ";";
    }

    return str;
}

/** Sub-function to add a suffix to the ids of the node and all its children.
 *
 *    @param node the node to change
 *    @param suffix the suffix to add
 *    @param replace dictionary of replaced ids
 *  @see suffixNodeIds
 */
function suffixNoneIds_sub(node, suffix, replace)
{
    if (node.nodeType == 1)
    {
        if (node.getAttribute("id"))
        {
            var id = node.getAttribute("id")
                replace["#" + id] = id + suffix;
            node.setAttribute("id", id + suffix);
        }

        if ((node.nodeName == "use") && (node.getAttributeNS(NSS["xlink"], "href")) && (replace[node.getAttribute(NSS["xlink"], "href")]))
            node.setAttribute(NSS["xlink"], "href", node.getAttribute(NSS["xlink"], "href") + suffix);

        if (node.childNodes)
        {
            for (var counter = 0; counter < node.childNodes.length; counter++)
                suffixNoneIds_sub(node.childNodes[counter], suffix, replace);
        }
    }
}

/** Function to add a suffix to the ids of the node and all its children.
 *
 *    @param node the node to change
 *    @param suffix the suffix to add
 *  @return the changed node
 *  @see suffixNodeIds_sub
 */
function suffixNodeIds(node, suffix)
{
    var replace = new Object();

    suffixNoneIds_sub(node, suffix, replace);

    return node;
}

/** Function to build a progress bar.
 *
 *  @param parent node to attach the progress bar to
 */
function createProgressBar(parent_node)
{
    var g = document.createElementNS(NSS["svg"], "g");
    g.setAttribute("clip-path", "url(#jessyInkSlideClipPath)");
    g.setAttribute("id", "layer_progress_bar");
    g.setAttribute("style", "display: none;");

    var rect_progress_bar = document.createElementNS(NSS["svg"], "rect");
    rect_progress_bar.setAttribute("style", "marker: none; fill: rgb(128, 128, 128); stroke: none;");
    rect_progress_bar.setAttribute("id", "rect_progress_bar");
    rect_progress_bar.setAttribute("x", 0);
    rect_progress_bar.setAttribute("y", 0.99 * HEIGHT);
    rect_progress_bar.setAttribute("width", 0);
    rect_progress_bar.setAttribute("height", 0.01 * HEIGHT);
    g.appendChild(rect_progress_bar);

    var circle_timer_indicator = document.createElementNS(NSS["svg"], "circle");
    circle_timer_indicator.setAttribute("style", "marker: none; fill: rgb(255, 0, 0); stroke: none;");
    circle_timer_indicator.setAttribute("id", "circle_timer_indicator");
    circle_timer_indicator.setAttribute("cx", 0.005 * HEIGHT);
    circle_timer_indicator.setAttribute("cy", 0.995 * HEIGHT);
    circle_timer_indicator.setAttribute("r", 0.005 * HEIGHT);
    g.appendChild(circle_timer_indicator);

    parent_node.appendChild(g);
}

/** Function to hide the progress bar.
 *
 */
function hideProgressBar()
{
    var progress_bar = document.getElementById("layer_progress_bar");

    if (!progress_bar)
    {
        return;
    }

    progress_bar.setAttribute("style", "display: none;");
}

/** Function to show the progress bar.
 *
 */
function showProgressBar()
{
    var progress_bar = document.getElementById("layer_progress_bar");

    if (!progress_bar)
    {
        return;
    }

    progress_bar.setAttribute("style", "display: inherit;");
}

/** Set progress bar value.
 *
 *    @param value the current slide number
 *
 */
function setProgressBarValue(value)
{
    var rect_progress_bar = document.getElementById("rect_progress_bar");

    if (!rect_progress_bar)
    {
        return;
    }

    if (value < 1)
    {
        // First slide, assumed to be the title of the presentation
        var x = 0;
        var w = 0.01 * HEIGHT;
    }
    else if (value >= slides.length - 1)
    {
        // Last slide, assumed to be the end of the presentation
        var x = WIDTH - 0.01 * HEIGHT;
        var w = 0.01 * HEIGHT;
    }
    else
    {
        value -= 1;
        value /= (slides.length - 2);

        var x = WIDTH * value;
        var w = WIDTH / (slides.length - 2);
    }

    rect_progress_bar.setAttribute("x", x);
    rect_progress_bar.setAttribute("width", w);
}

/** Set time indicator.
 *
 *    @param value the percentage of time elapse so far between 0.0 and 1.0
 *
 */
function setTimeIndicatorValue(value)
{
    var circle_timer_indicator = document.getElementById("circle_timer_indicator");

    if (!circle_timer_indicator)
    {
        return;
    }

    if (value < 0.0)
    {
        value = 0.0;
    }

    if (value > 1.0)
    {
        value = 1.0;
    }

    var cx = (WIDTH - 0.01 * HEIGHT) * value + 0.005 * HEIGHT;
    circle_timer_indicator.setAttribute("cx", cx);
}

/** Update timer.
 *
 */
function updateTimer()
{
    timer_elapsed += 1;
    setTimeIndicatorValue((timer_elapsed - timer_start) / (60 * timer_duration));
}

/** Convert screen coordinates to document coordinates.
 *
 *  @param e event with screen coordinates
 *
 *  @return coordinates in SVG file coordinate system
 */
function calcCoord(e)
{
    var svgPoint = document.documentElement.createSVGPoint();
    svgPoint.x = e.clientX + window.pageXOffset;
    svgPoint.y = e.clientY + window.pageYOffset;

    var matrix = slides[activeSlide]["element"].getScreenCTM();

    if (slides[activeSlide]["viewGroup"])
        matrix = slides[activeSlide]["viewGroup"].getScreenCTM();

    svgPoint = svgPoint.matrixTransform(matrix.inverse());
    return svgPoint;
}

/** Add slide.
 *
 *    @param after_slide after which slide the new slide should be inserted into the presentation
 */
function addSlide(after_slide)
{
    number_of_added_slides++;

    var g = document.createElementNS(NSS["svg"], "g");
    g.setAttribute("clip-path", "url(#jessyInkSlideClipPath)");
    g.setAttribute("id", "Whiteboard " + Date() + " presentation copy");
    g.setAttribute("style", "display: none;");

    var new_slide = new Object();
    new_slide["element"] = g;

    // Set build in transition.
    new_slide["transitionIn"] = new Object();
    var dict = defaultTransitionInDict;
    new_slide["transitionIn"]["name"] = dict["name"];
    new_slide["transitionIn"]["options"] = new Object();

    for (key in dict)
        if (key != "name")
            new_slide["transitionIn"]["options"][key] = dict[key];

    // Set build out transition.
    new_slide["transitionOut"] = new Object();
    dict = defaultTransitionOutDict;
    new_slide["transitionOut"]["name"] = dict["name"];
    new_slide["transitionOut"]["options"] = new Object();

    for (key in dict)
        if (key != "name")
            new_slide["transitionOut"]["options"][key] = dict[key];

    // Copy master slide content.
    if (masterSlide)
    {
        var clonedNode = suffixNodeIds(masterSlide.cloneNode(true), "_" + Date() + " presentation_copy");
        clonedNode.removeAttributeNS(NSS["inkscape"], "groupmode");
        clonedNode.removeAttributeNS(NSS["inkscape"], "label");
        clonedNode.style.display = "inherit";

        g.appendChild(clonedNode);
    }

    // Substitute auto texts.
    substituteAutoTexts(g, "Whiteboard " + number_of_added_slides, "W" + number_of_added_slides, slides.length);

    g.setAttribute("onmouseover", "if ((currentMode == INDEX_MODE) && ( activeSlide != " + (after_slide + 1) + ")) { indexSetActiveSlide(" + (after_slide + 1) + "); };");

    // Create a transform group.
    var transformGroup = document.createElementNS(NSS["svg"], "g");

    // Add content to transform group.
    while (g.firstChild)
        transformGroup.appendChild(g.firstChild);

    // Transfer the transform attribute from the node to the transform group.
    if (g.getAttribute("transform"))
    {
        transformGroup.setAttribute("transform", g.getAttribute("transform"));
        g.removeAttribute("transform");
    }

    // Create a view group.
    var viewGroup = document.createElementNS(NSS["svg"], "g");

    viewGroup.appendChild(transformGroup);
    new_slide["viewGroup"] = g.appendChild(viewGroup);

    // Insert background.
    if (BACKGROUND_COLOR != null)
    {
        var rectNode = document.createElementNS(NSS["svg"], "rect");

        rectNode.setAttribute("x", 0);
        rectNode.setAttribute("y", 0);
        rectNode.setAttribute("width", WIDTH);
        rectNode.setAttribute("height", HEIGHT);
        rectNode.setAttribute("id", "jessyInkBackground" + Date());
        rectNode.setAttribute("fill", BACKGROUND_COLOR);

        new_slide["viewGroup"].insertBefore(rectNode, new_slide["viewGroup"].firstChild);
    }

    // Set initial view even if there are no other views.
    var matrixOld = (new matrixSVG()).fromElements(1, 0, 0, 0, 1, 0, 0, 0, 1);

    new_slide["viewGroup"].setAttribute("transform", matrixOld.toAttribute());
    new_slide.initialView = matrixOld.toAttribute();

    // Insert slide
    var node = slides[after_slide]["element"];
    var next_node = node.nextSibling;
    var parent_node = node.parentNode;

    if (next_node)
    {
        parent_node.insertBefore(g, next_node);
    }
    else
    {
        parent_node.appendChild(g);
    }

    g = document.createElementNS(NSS["svg"], "g");
    g.setAttributeNS(NSS["inkscape"], "groupmode", "layer");
    g.setAttributeNS(NSS["inkscape"], "label", "Whiteboard " + number_of_added_slides);
    g.setAttribute("clip-path", "url(#jessyInkSlideClipPath)");
    g.setAttribute("id", "Whiteboard " + Date());
    g.setAttribute("style", "display: none;");

    new_slide["original_element"] = g;

    node = slides[after_slide]["original_element"];
    next_node = node.nextSibling;
    parent_node = node.parentNode;

    if (next_node)
    {
        parent_node.insertBefore(g, next_node);
    }
    else
    {
        parent_node.appendChild(g);
    }

    before_new_slide = slides.slice(0, after_slide + 1);
    after_new_slide = slides.slice(after_slide + 1);
    slides = before_new_slide.concat(new_slide, after_new_slide);

    //resetting the counter attributes on the slides that follow the new slide...
    for (var counter = after_slide+2; counter < slides.length; counter++)
    {
        slides[counter]["element"].setAttribute("onmouseover", "if ((currentMode == INDEX_MODE) && ( activeSlide != " + counter + ")) { indexSetActiveSlide(" + counter + "); };");
    }
}

/** Convenience function to obtain a transformation matrix from a point matrix.
 *
 *    @param mPoints Point matrix.
 *    @return A transformation matrix.
 */
function pointMatrixToTransformation(mPoints)
{
    mPointsOld = (new matrixSVG()).fromElements(0, WIDTH, WIDTH, 0, 0, HEIGHT, 1, 1, 1);

    return mPointsOld.mult(mPoints.inv());
}

/** Convenience function to obtain a matrix with three corners of a rectangle.
 *
 *    @param rect an svg rectangle
 *    @return a matrixSVG containing three corners of the rectangle
 */
function rectToMatrix(rect)
{
    rectWidth = rect.getBBox().width;
    rectHeight = rect.getBBox().height;
    rectX = rect.getBBox().x;
    rectY = rect.getBBox().y;
    rectXcorr = 0;
    rectYcorr = 0;

    scaleX = WIDTH / rectWidth;
    scaleY = HEIGHT / rectHeight;

    if (scaleX > scaleY)
    {
        scaleX = scaleY;
        rectXcorr -= (WIDTH / scaleX - rectWidth) / 2;
        rectWidth = WIDTH / scaleX;
    }
    else
    {
        scaleY = scaleX;
        rectYcorr -= (HEIGHT / scaleY - rectHeight) / 2;
        rectHeight = HEIGHT / scaleY;
    }

    if (rect.transform.baseVal.numberOfItems < 1)
    {
        mRectTrans = (new matrixSVG()).fromElements(1, 0, 0, 0, 1, 0, 0, 0, 1);
    }
    else
    {
        mRectTrans = (new matrixSVG()).fromSVGMatrix(rect.transform.baseVal.consolidate().matrix);
    }

    newBasePoints = (new matrixSVG()).fromElements(rectX, rectX, rectX, rectY, rectY, rectY, 1, 1, 1);
    newVectors = (new matrixSVG()).fromElements(rectXcorr, rectXcorr + rectWidth, rectXcorr + rectWidth, rectYcorr, rectYcorr, rectYcorr + rectHeight, 0, 0, 0);

    return mRectTrans.mult(newBasePoints.add(newVectors));
}

/** Function to handle JessyInk elements.
 *
 *    @param    node    Element node.
 */
function handleElement(node)
{
    if (node.getAttributeNS(NSS['jessyink'], 'element') == 'core.video')
    {
        var url;
        var width;
        var height;
        var x;
        var y;
        var transform;

        var tspans = node.getElementsByTagNameNS("http://www.w3.org/2000/svg", "tspan");

        for (var tspanCounter = 0; tspanCounter < tspans.length; tspanCounter++)
        {
            if (tspans[tspanCounter].getAttributeNS("https://launchpad.net/jessyink", "video") == "url")
            {
                url = tspans[tspanCounter].firstChild.nodeValue;
            }
        }

        var rects = node.getElementsByTagNameNS("http://www.w3.org/2000/svg", "rect");

        for (var rectCounter = 0; rectCounter < rects.length; rectCounter++)
        {
            if (rects[rectCounter].getAttributeNS("https://launchpad.net/jessyink", "video") == "rect")
            {
                x = rects[rectCounter].getAttribute("x");
                y = rects[rectCounter].getAttribute("y");
                width = rects[rectCounter].getAttribute("width");
                height = rects[rectCounter].getAttribute("height");
                transform = rects[rectCounter].getAttribute("transform");
            }
        }

        for (var childCounter = 0; childCounter < node.childNodes.length; childCounter++)
        {
            if (node.childNodes[childCounter].nodeType == 1)
            {
                if (node.childNodes[childCounter].style)
                {
                    node.childNodes[childCounter].style.display = 'none';
                }
                else
                {
                    node.childNodes[childCounter].setAttribute("style", "display: none;");
                }
            }
        }

        var foreignNode = document.createElementNS("http://www.w3.org/2000/svg", "foreignObject");
        foreignNode.setAttribute("x", x);
        foreignNode.setAttribute("y", y);
        foreignNode.setAttribute("width", width);
        foreignNode.setAttribute("height", height);
        foreignNode.setAttribute("transform", transform);

        var videoNode = document.createElementNS("http://www.w3.org/1999/xhtml", "video");
        videoNode.setAttribute("src", url);

        foreignNode.appendChild(videoNode);
        node.appendChild(foreignNode);
    }
}

/** Class processing the location hash.
 *
 *    @param str location hash
 */
function LocationHash(str)
{
    this.slideNumber = 0;
    this.effectNumber = 0;

    str = str.substr(1, str.length - 1);

    var parts = str.split('_');

    // Try to extract slide number.
    if (parts.length >= 1)
    {
        try
        {
            var slideNumber = parseInt(parts[0]);

            if (!isNaN(slideNumber))
            {
                this.slideNumber = slideNumber - 1;
            }
        }
        catch (e)
        {
        }
    }

    // Try to extract effect number.
    if (parts.length >= 2)
    {
        try
        {
            var effectNumber = parseInt(parts[1]);

            if (!isNaN(effectNumber))
            {
                this.effectNumber = effectNumber;
            }
        }
        catch (e)
        {
        }
    }
}

/** Class representing an svg matrix.
*/
function matrixSVG()
{
    this.e11 = 0; // a
    this.e12 = 0; // c
    this.e13 = 0; // e
    this.e21 = 0; // b
    this.e22 = 0; // d
    this.e23 = 0; // f
    this.e31 = 0;
    this.e32 = 0;
    this.e33 = 0;
}

/** Constructor function.
 *
 *    @param a element a (i.e. 1, 1) as described in the svg standard.
 *    @param b element b (i.e. 2, 1) as described in the svg standard.
 *    @param c element c (i.e. 1, 2) as described in the svg standard.
 *    @param d element d (i.e. 2, 2) as described in the svg standard.
 *    @param e element e (i.e. 1, 3) as described in the svg standard.
 *    @param f element f (i.e. 2, 3) as described in the svg standard.
 */
matrixSVG.prototype.fromSVGElements = function(a, b, c, d, e, f)
{
    this.e11 = a;
    this.e12 = c;
    this.e13 = e;
    this.e21 = b;
    this.e22 = d;
    this.e23 = f;
    this.e31 = 0;
    this.e32 = 0;
    this.e33 = 1;

    return this;
}

/** Constructor function.
 *
 *    @param matrix an svg matrix as described in the svg standard.
 */
matrixSVG.prototype.fromSVGMatrix = function(m)
{
    this.e11 = m.a;
    this.e12 = m.c;
    this.e13 = m.e;
    this.e21 = m.b;
    this.e22 = m.d;
    this.e23 = m.f;
    this.e31 = 0;
    this.e32 = 0;
    this.e33 = 1;

    return this;
}

/** Constructor function.
 *
 *    @param e11 element 1, 1 of the matrix.
 *    @param e12 element 1, 2 of the matrix.
 *    @param e13 element 1, 3 of the matrix.
 *    @param e21 element 2, 1 of the matrix.
 *    @param e22 element 2, 2 of the matrix.
 *    @param e23 element 2, 3 of the matrix.
 *    @param e31 element 3, 1 of the matrix.
 *    @param e32 element 3, 2 of the matrix.
 *    @param e33 element 3, 3 of the matrix.
 */
matrixSVG.prototype.fromElements = function(e11, e12, e13, e21, e22, e23, e31, e32, e33)
{
    this.e11 = e11;
    this.e12 = e12;
    this.e13 = e13;
    this.e21 = e21;
    this.e22 = e22;
    this.e23 = e23;
    this.e31 = e31;
    this.e32 = e32;
    this.e33 = e33;

    return this;
}

/** Constructor function.
 *
 *    @param attrString string value of the "transform" attribute (currently only "matrix" is accepted)
 */
matrixSVG.prototype.fromAttribute = function(attrString)
{
    str = attrString.substr(7, attrString.length - 8);

    str = str.trim();

    strArray = str.split(",");

    // Opera does not use commas to separate the values of the matrix, only spaces.
    if (strArray.length != 6)
        strArray = str.split(" ");

    this.e11 = parseFloat(strArray[0]);
    this.e21 = parseFloat(strArray[1]);
    this.e31 = 0;
    this.e12 = parseFloat(strArray[2]);
    this.e22 = parseFloat(strArray[3]);
    this.e32 = 0;
    this.e13 = parseFloat(strArray[4]);
    this.e23 = parseFloat(strArray[5]);
    this.e33 = 1;

    return this;
}

/** Output function
 *
 *    @return a string that can be used as the "transform" attribute.
 */
matrixSVG.prototype.toAttribute = function()
{
    return "matrix(" + this.e11 + ", " + this.e21 + ", " + this.e12 + ", " + this.e22 + ", " + this.e13 + ", " + this.e23 + ")";
}

/** Matrix nversion.
 *
 *    @return the inverse of the matrix
 */
matrixSVG.prototype.inv = function()
{
    out = new matrixSVG();

    det = this.e11 * (this.e33 * this.e22 - this.e32 * this.e23) - this.e21 * (this.e33 * this.e12 - this.e32 * this.e13) + this.e31 * (this.e23 * this.e12 - this.e22 * this.e13);

    out.e11 =  (this.e33 * this.e22 - this.e32 * this.e23) / det;
    out.e12 = -(this.e33 * this.e12 - this.e32 * this.e13) / det;
    out.e13 =  (this.e23 * this.e12 - this.e22 * this.e13) / det;
    out.e21 = -(this.e33 * this.e21 - this.e31 * this.e23) / det;
    out.e22 =  (this.e33 * this.e11 - this.e31 * this.e13) / det;
    out.e23 = -(this.e23 * this.e11 - this.e21 * this.e13) / det;
    out.e31 =  (this.e32 * this.e21 - this.e31 * this.e22) / det;
    out.e32 = -(this.e32 * this.e11 - this.e31 * this.e12) / det;
    out.e33 =  (this.e22 * this.e11 - this.e21 * this.e12) / det;

    return out;
}

/** Matrix multiplication.
 *
 *    @param op another svg matrix
 *    @return this * op
 */
matrixSVG.prototype.mult = function(op)
{
    out = new matrixSVG();

    out.e11 = this.e11 * op.e11 + this.e12 * op.e21 + this.e13 * op.e31;
    out.e12 = this.e11 * op.e12 + this.e12 * op.e22 + this.e13 * op.e32;
    out.e13 = this.e11 * op.e13 + this.e12 * op.e23 + this.e13 * op.e33;
    out.e21 = this.e21 * op.e11 + this.e22 * op.e21 + this.e23 * op.e31;
    out.e22 = this.e21 * op.e12 + this.e22 * op.e22 + this.e23 * op.e32;
    out.e23 = this.e21 * op.e13 + this.e22 * op.e23 + this.e23 * op.e33;
    out.e31 = this.e31 * op.e11 + this.e32 * op.e21 + this.e33 * op.e31;
    out.e32 = this.e31 * op.e12 + this.e32 * op.e22 + this.e33 * op.e32;
    out.e33 = this.e31 * op.e13 + this.e32 * op.e23 + this.e33 * op.e33;

    return out;
}

/** Matrix addition.
 *
 *    @param op another svg matrix
 *    @return this + op
 */
matrixSVG.prototype.add = function(op)
{
    out = new matrixSVG();

    out.e11 = this.e11 + op.e11;
    out.e12 = this.e12 + op.e12;
    out.e13 = this.e13 + op.e13;
    out.e21 = this.e21 + op.e21;
    out.e22 = this.e22 + op.e22;
    out.e23 = this.e23 + op.e23;
    out.e31 = this.e31 + op.e31;
    out.e32 = this.e32 + op.e32;
    out.e33 = this.e33 + op.e33;

    return out;
}

/** Matrix mixing.
 *
 *    @param op another svg matrix
 *    @parma contribOp contribution of the other matrix (0 <= contribOp <= 1)
 *    @return (1 - contribOp) * this + contribOp * op
 */
matrixSVG.prototype.mix = function(op, contribOp)
{
    contribThis = 1.0 - contribOp;
    out = new matrixSVG();

    out.e11 = contribThis * this.e11 + contribOp * op.e11;
    out.e12 = contribThis * this.e12 + contribOp * op.e12;
    out.e13 = contribThis * this.e13 + contribOp * op.e13;
    out.e21 = contribThis * this.e21 + contribOp * op.e21;
    out.e22 = contribThis * this.e22 + contribOp * op.e22;
    out.e23 = contribThis * this.e23 + contribOp * op.e23;
    out.e31 = contribThis * this.e31 + contribOp * op.e31;
    out.e32 = contribThis * this.e32 + contribOp * op.e32;
    out.e33 = contribThis * this.e33 + contribOp * op.e33;

    return out;
}

/** Trimming function for strings.
*/
String.prototype.trim = function()
{
    return this.replace(/^\s+|\s+$/g, '');
}

/** SVGElement.getTransformToElement polyfill */
SVGElement.prototype.getTransformToElement = SVGElement.prototype.getTransformToElement || function(elem) {
    return elem.getScreenCTM().inverse().multiply(this.getScreenCTM());
};
