/* -*- Mode: JS; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */

/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 *                      - Presentation Engine -                            *
 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /

/**
 *  WARNING: any comment that should not be striped out by the script
 *  generating the C++ header file must start with a '/' and exactly 5 '*'
 *  not striped examples: '/*****', '/***** *'
 *  striped examples: '/** ***' (not contiguous), '/******' (more than 5)
 *
 *  NOTE: This file combines several works, under different
 *  licenses. See the @licstart / @licend sections below.
 */

/*! Hammer.JS - v2.0.7 - 2016-04-22
 * http://hammerjs.github.io/
 *
 * Copyright (c) 2016 Jorik Tangelder;
 * Licensed under the MIT license */
(function(window, document, exportName, undefined) {
  'use strict';

var VENDOR_PREFIXES = ['', 'webkit', 'Moz', 'MS', 'ms', 'o'];
var TEST_ELEMENT = document.createElement('div');

var TYPE_FUNCTION = 'function';

var round = Math.round;
var abs = Math.abs;
var now = Date.now;

/**
 * polyfill for IE11
 */
if (!Math.trunc) {
    Math.trunc = function (v) {
        return v < 0 ? Math.ceil(v) : Math.floor(v);
    };
}

/**
 * set a timeout with a given scope
 * @param {Function} fn
 * @param {Number} timeout
 * @param {Object} context
 * @returns {number}
 */
function setTimeoutContext(fn, timeout, context) {
    return setTimeout(bindFn(fn, context), timeout);
}

/**
 * if the argument is an array, we want to execute the fn on each entry
 * if it aint an array we don't want to do a thing.
 * this is used by all the methods that accept a single and array argument.
 * @param {*|Array} arg
 * @param {String} fn
 * @param {Object} [context]
 * @returns {Boolean}
 */
function invokeArrayArg(arg, fn, context) {
    if (Array.isArray(arg)) {
        each(arg, context[fn], context);
        return true;
    }
    return false;
}

/**
 * walk objects and arrays
 * @param {Object} obj
 * @param {Function} iterator
 * @param {Object} context
 */
function each(obj, iterator, context) {
    var i;

    if (!obj) {
        return;
    }

    if (obj.forEach) {
        obj.forEach(iterator, context);
    } else if (obj.length !== undefined) {
        i = 0;
        while (i < obj.length) {
            iterator.call(context, obj[i], i, obj);
            i++;
        }
    } else {
        for (i in obj) {
            obj.hasOwnProperty(i) && iterator.call(context, obj[i], i, obj);
        }
    }
}

/**
 * wrap a method with a deprecation warning and stack trace
 * @param {Function} method
 * @param {String} name
 * @param {String} message
 * @returns {Function} A new function wrapping the supplied method.
 */
function deprecate(method, name, message) {
    var deprecationMessage = 'DEPRECATED METHOD: ' + name + '\n' + message + ' AT \n';
    return function() {
        var e = new Error('get-stack-trace');
        var stack = e && e.stack ? e.stack.replace(/^[^\(]+?[\n$]/gm, '')
            .replace(/^\s+at\s+/gm, '')
            .replace(/^Object.<anonymous>\s*\(/gm, '{anonymous}()@') : 'Unknown Stack Trace';

        var log = window.console && (window.console.warn || window.console.log);
        if (log) {
            log.call(window.console, deprecationMessage, stack);
        }
        return method.apply(this, arguments);
    };
}

/**
 * extend object.
 * means that properties in dest will be overwritten by the ones in src.
 * @param {Object} target
 * @param {...Object} objects_to_assign
 * @returns {Object} target
 */
var assign;
if (typeof Object.assign !== 'function') {
    assign = function assign(target) {
        if (target === undefined || target === null) {
            throw new TypeError('Cannot convert undefined or null to object');
        }

        var output = Object(target);
        for (var index = 1; index < arguments.length; index++) {
            var source = arguments[index];
            if (source !== undefined && source !== null) {
                for (var nextKey in source) {
                    if (source.hasOwnProperty(nextKey)) {
                        output[nextKey] = source[nextKey];
                    }
                }
            }
        }
        return output;
    };
} else {
    assign = Object.assign;
}

/**
 * extend object.
 * means that properties in dest will be overwritten by the ones in src.
 * @param {Object} dest
 * @param {Object} src
 * @param {Boolean} [merge=false]
 * @returns {Object} dest
 */
var extend = deprecate(function extend(dest, src, merge) {
    var keys = Object.keys(src);
    var i = 0;
    while (i < keys.length) {
        if (!merge || (merge && dest[keys[i]] === undefined)) {
            dest[keys[i]] = src[keys[i]];
        }
        i++;
    }
    return dest;
}, 'extend', 'Use `assign`.');

/**
 * merge the values from src in the dest.
 * means that properties that exist in dest will not be overwritten by src
 * @param {Object} dest
 * @param {Object} src
 * @returns {Object} dest
 */
var merge = deprecate(function merge(dest, src) {
    return extend(dest, src, true);
}, 'merge', 'Use `assign`.');

/**
 * simple class inheritance
 * @param {Function} child
 * @param {Function} base
 * @param {Object} [properties]
 */
function inherit(child, base, properties) {
    var baseP = base.prototype,
        childP;

    childP = child.prototype = Object.create(baseP);
    childP.constructor = child;
    childP._super = baseP;

    if (properties) {
        assign(childP, properties);
    }
}

/**
 * simple function bind
 * @param {Function} fn
 * @param {Object} context
 * @returns {Function}
 */
function bindFn(fn, context) {
    return function boundFn() {
        return fn.apply(context, arguments);
    };
}

/**
 * let a boolean value also be a function that must return a boolean
 * this first item in args will be used as the context
 * @param {Boolean|Function} val
 * @param {Array} [args]
 * @returns {Boolean}
 */
function boolOrFn(val, args) {
    if (typeof val == TYPE_FUNCTION) {
        return val.apply(args ? args[0] || undefined : undefined, args);
    }
    return val;
}

/**
 * use the val2 when val1 is undefined
 * @param {*} val1
 * @param {*} val2
 * @returns {*}
 */
function ifUndefined(val1, val2) {
    return (val1 === undefined) ? val2 : val1;
}

/**
 * addEventListener with multiple events at once
 * @param {EventTarget} target
 * @param {String} types
 * @param {Function} handler
 */
function addEventListeners(target, types, handler) {
    each(splitStr(types), function(type) {
        target.addEventListener(type, handler, false);
    });
}

/**
 * removeEventListener with multiple events at once
 * @param {EventTarget} target
 * @param {String} types
 * @param {Function} handler
 */
function removeEventListeners(target, types, handler) {
    each(splitStr(types), function(type) {
        target.removeEventListener(type, handler, false);
    });
}

/**
 * find if a node is in the given parent
 * @method hasParent
 * @param {HTMLElement} node
 * @param {HTMLElement} parent
 * @return {Boolean} found
 */
function hasParent(node, parent) {
    while (node) {
        if (node == parent) {
            return true;
        }
        node = node.parentNode;
    }
    return false;
}

/**
 * small indexOf wrapper
 * @param {String} str
 * @param {String} find
 * @returns {Boolean} found
 */
function inStr(str, find) {
    return str.indexOf(find) > -1;
}

/**
 * split string on whitespace
 * @param {String} str
 * @returns {Array} words
 */
function splitStr(str) {
    return str.trim().split(/\s+/g);
}

/**
 * find if an array contains the object using indexOf or a simple polyFill
 * @param {Array} src
 * @param {String} find
 * @param {String} [findByKey]
 * @return {Boolean|Number} false when not found, or the index
 */
function inArray(src, find, findByKey) {
    if (src.indexOf && !findByKey) {
        return src.indexOf(find);
    } else {
        var i = 0;
        while (i < src.length) {
            if ((findByKey && src[i][findByKey] == find) || (!findByKey && src[i] === find)) {
                return i;
            }
            i++;
        }
        return -1;
    }
}

/**
 * convert array-like objects to real arrays
 * @param {Object} obj
 * @returns {Array}
 */
function toArray(obj) {
    return Array.prototype.slice.call(obj, 0);
}

/**
 * unique array with objects based on a key (like 'id') or just by the array's value
 * @param {Array} src [{id:1},{id:2},{id:1}]
 * @param {String} [key]
 * @param {Boolean} [sort=False]
 * @returns {Array} [{id:1},{id:2}]
 */
function uniqueArray(src, key, sort) {
    var results = [];
    var values = [];
    var i = 0;

    while (i < src.length) {
        var val = key ? src[i][key] : src[i];
        if (inArray(values, val) < 0) {
            results.push(src[i]);
        }
        values[i] = val;
        i++;
    }

    if (sort) {
        if (!key) {
            results = results.sort();
        } else {
            results = results.sort(function sortUniqueArray(a, b) {
                return a[key] > b[key];
            });
        }
    }

    return results;
}

/**
 * get the prefixed property
 * @param {Object} obj
 * @param {String} property
 * @returns {String|Undefined} prefixed
 */
function prefixed(obj, property) {
    // tml: Have to check for obj being undefined
    if (obj === undefined) {
        return undefined;
    }

    var prefix, prop;
    var camelProp = property[0].toUpperCase() + property.slice(1);

    var i = 0;
    while (i < VENDOR_PREFIXES.length) {
        prefix = VENDOR_PREFIXES[i];
        prop = (prefix) ? prefix + camelProp : property;

        if (prop in obj) {
            return prop;
        }
        i++;
    }
    return undefined;
}

/**
 * get a unique id
 * @returns {number} uniqueId
 */
var _uniqueId = 1;
function uniqueId() {
    return _uniqueId++;
}

/**
 * get the window object of an element
 * @param {HTMLElement} element
 * @returns {DocumentView|Window}
 */
function getWindowForElement(element) {
    var doc = element.ownerDocument || element;
    return (doc.defaultView || doc.parentWindow || window);
}

var MOBILE_REGEX = /mobile|tablet|ip(ad|hone|od)|android/i;

var SUPPORT_TOUCH = ('ontouchstart' in window);
var SUPPORT_POINTER_EVENTS = prefixed(window, 'PointerEvent') !== undefined;
var SUPPORT_ONLY_TOUCH = SUPPORT_TOUCH && MOBILE_REGEX.test(navigator.userAgent);

var INPUT_TYPE_TOUCH = 'touch';
var INPUT_TYPE_PEN = 'pen';
var INPUT_TYPE_MOUSE = 'mouse';
var INPUT_TYPE_KINECT = 'kinect';

var COMPUTE_INTERVAL = 25;

var INPUT_START = 1;
var INPUT_MOVE = 2;
var INPUT_END = 4;
var INPUT_CANCEL = 8;

var DIRECTION_NONE = 1;
var DIRECTION_LEFT = 2;
var DIRECTION_RIGHT = 4;
var DIRECTION_UP = 8;
var DIRECTION_DOWN = 16;

var DIRECTION_HORIZONTAL = DIRECTION_LEFT | DIRECTION_RIGHT;
var DIRECTION_VERTICAL = DIRECTION_UP | DIRECTION_DOWN;
var DIRECTION_ALL = DIRECTION_HORIZONTAL | DIRECTION_VERTICAL;

var PROPS_XY = ['x', 'y'];
var PROPS_CLIENT_XY = ['clientX', 'clientY'];

/**
 * create new input type manager
 * @param {Manager} manager
 * @param {Function} callback
 * @returns {Input}
 * @constructor
 */
function Input(manager, callback) {
    var self = this;
    this.manager = manager;
    this.callback = callback;
    this.element = manager.element;
    this.target = manager.options.inputTarget;

    // smaller wrapper around the handler, for the scope and the enabled state of the manager,
    // so when disabled the input events are completely bypassed.
    this.domHandler = function(ev) {
        if (boolOrFn(manager.options.enable, [manager])) {
            self.handler(ev);
        }
    };

    this.init();

}

Input.prototype = {
    /**
     * should handle the inputEvent data and trigger the callback
     * @virtual
     */
    handler: function() { },

    /**
     * bind the events
     */
    init: function() {
        this.evEl && addEventListeners(this.element, this.evEl, this.domHandler);
        this.evTarget && addEventListeners(this.target, this.evTarget, this.domHandler);
        this.evWin && addEventListeners(getWindowForElement(this.element), this.evWin, this.domHandler);
    },

    /**
     * unbind the events
     */
    destroy: function() {
        this.evEl && removeEventListeners(this.element, this.evEl, this.domHandler);
        this.evTarget && removeEventListeners(this.target, this.evTarget, this.domHandler);
        this.evWin && removeEventListeners(getWindowForElement(this.element), this.evWin, this.domHandler);
    }
};

/**
 * create new input type manager
 * called by the Manager constructor
 * @param {Hammer} manager
 * @returns {Input}
 */
function createInputInstance(manager) {
    var Type;
    var inputClass = manager.options.inputClass;

    if (inputClass) {
        Type = inputClass;
    } else if (!SUPPORT_TOUCH && SUPPORT_POINTER_EVENTS) {
        Type = PointerEventInput;
    } else if (SUPPORT_ONLY_TOUCH) {
        Type = TouchInput;
    } else if (!SUPPORT_TOUCH) {
        Type = MouseInput;
    } else {
        Type = TouchMouseInput;
    }
    return new (Type)(manager, inputHandler);
}

/**
 * handle input events
 * @param {Manager} manager
 * @param {String} eventType
 * @param {Object} input
 */
function inputHandler(manager, eventType, input) {
    var pointersLen = input.pointers.length;
    var changedPointersLen = input.changedPointers.length;
    var isFirst = (eventType & INPUT_START && (pointersLen - changedPointersLen === 0));
    var isFinal = (eventType & (INPUT_END | INPUT_CANCEL) && (pointersLen - changedPointersLen === 0));

    input.isFirst = !!isFirst;
    input.isFinal = !!isFinal;

    if (isFirst) {
        manager.session = {};
    }

    // source event is the normalized value of the domEvents
    // like 'touchstart, mouseup, pointerdown'
    input.eventType = eventType;

    // compute scale, rotation etc
    computeInputData(manager, input);

    // emit secret event
    manager.emit('hammer.input', input);

    manager.recognize(input);
    manager.session.prevInput = input;
}

/**
 * extend the data with some usable properties like scale, rotate, velocity etc
 * @param {Object} manager
 * @param {Object} input
 */
function computeInputData(manager, input) {
    var session = manager.session;
    var pointers = input.pointers;
    var pointersLength = pointers.length;

    // store the first input to calculate the distance and direction
    if (!session.firstInput) {
        session.firstInput = simpleCloneInputData(input);
    }

    // to compute scale and rotation we need to store the multiple touches
    if (pointersLength > 1 && !session.firstMultiple) {
        session.firstMultiple = simpleCloneInputData(input);
    } else if (pointersLength === 1) {
        session.firstMultiple = false;
    }

    var firstInput = session.firstInput;
    var firstMultiple = session.firstMultiple;
    var offsetCenter = firstMultiple ? firstMultiple.center : firstInput.center;

    var center = input.center = getCenter(pointers);
    input.timeStamp = now();
    input.deltaTime = input.timeStamp - firstInput.timeStamp;

    input.angle = getAngle(offsetCenter, center);
    input.distance = getDistance(offsetCenter, center);

    computeDeltaXY(session, input);
    input.offsetDirection = getDirection(input.deltaX, input.deltaY);

    var overallVelocity = getVelocity(input.deltaTime, input.deltaX, input.deltaY);
    input.overallVelocityX = overallVelocity.x;
    input.overallVelocityY = overallVelocity.y;
    input.overallVelocity = (abs(overallVelocity.x) > abs(overallVelocity.y)) ? overallVelocity.x : overallVelocity.y;

    input.scale = firstMultiple ? getScale(firstMultiple.pointers, pointers) : 1;
    input.rotation = firstMultiple ? getRotation(firstMultiple.pointers, pointers) : 0;

    input.maxPointers = !session.prevInput ? input.pointers.length : ((input.pointers.length >
        session.prevInput.maxPointers) ? input.pointers.length : session.prevInput.maxPointers);

    computeIntervalInputData(session, input);

    // find the correct target
    var target = manager.element;
    if (hasParent(input.srcEvent.target, target)) {
        target = input.srcEvent.target;
    }
    input.target = target;
}

function computeDeltaXY(session, input) {
    var center = input.center;
    var offset = session.offsetDelta || {};
    var prevDelta = session.prevDelta || {};
    var prevInput = session.prevInput || {};

    if (input.eventType === INPUT_START || prevInput.eventType === INPUT_END) {
        prevDelta = session.prevDelta = {
            x: prevInput.deltaX || 0,
            y: prevInput.deltaY || 0
        };

        offset = session.offsetDelta = {
            x: center.x,
            y: center.y
        };
    }

    input.deltaX = prevDelta.x + (center.x - offset.x);
    input.deltaY = prevDelta.y + (center.y - offset.y);
}

/**
 * velocity is calculated every x ms
 * @param {Object} session
 * @param {Object} input
 */
function computeIntervalInputData(session, input) {
    var last = session.lastInterval || input,
        deltaTime = input.timeStamp - last.timeStamp,
        velocity, velocityX, velocityY, direction;

    if (input.eventType != INPUT_CANCEL && (deltaTime > COMPUTE_INTERVAL || last.velocity === undefined)) {
        var deltaX = input.deltaX - last.deltaX;
        var deltaY = input.deltaY - last.deltaY;

        var v = getVelocity(deltaTime, deltaX, deltaY);
        velocityX = v.x;
        velocityY = v.y;
        velocity = (abs(v.x) > abs(v.y)) ? v.x : v.y;
        direction = getDirection(deltaX, deltaY);

        session.lastInterval = input;
    } else {
        // use latest velocity info if it doesn't overtake a minimum period
        velocity = last.velocity;
        velocityX = last.velocityX;
        velocityY = last.velocityY;
        direction = last.direction;
    }

    input.velocity = velocity;
    input.velocityX = velocityX;
    input.velocityY = velocityY;
    input.direction = direction;
}

/**
 * create a simple clone from the input used for storage of firstInput and firstMultiple
 * @param {Object} input
 * @returns {Object} clonedInputData
 */
function simpleCloneInputData(input) {
    // make a simple copy of the pointers because we will get a reference if we don't
    // we only need clientXY for the calculations
    var pointers = [];
    var i = 0;
    while (i < input.pointers.length) {
        pointers[i] = {
            clientX: round(input.pointers[i].clientX),
            clientY: round(input.pointers[i].clientY)
        };
        i++;
    }

    return {
        timeStamp: now(),
        pointers: pointers,
        center: getCenter(pointers),
        deltaX: input.deltaX,
        deltaY: input.deltaY
    };
}

/**
 * get the center of all the pointers
 * @param {Array} pointers
 * @return {Object} center contains `x` and `y` properties
 */
function getCenter(pointers) {
    var pointersLength = pointers.length;

    // no need to loop when only one touch
    if (pointersLength === 1) {
        return {
            x: round(pointers[0].clientX),
            y: round(pointers[0].clientY)
        };
    }

    var x = 0, y = 0, i = 0;
    while (i < pointersLength) {
        x += pointers[i].clientX;
        y += pointers[i].clientY;
        i++;
    }

    return {
        x: round(x / pointersLength),
        y: round(y / pointersLength)
    };
}

/**
 * calculate the velocity between two points. unit is in px per ms.
 * @param {Number} deltaTime
 * @param {Number} x
 * @param {Number} y
 * @return {Object} velocity `x` and `y`
 */
function getVelocity(deltaTime, x, y) {
    return {
        x: x / deltaTime || 0,
        y: y / deltaTime || 0
    };
}

/**
 * get the direction between two points
 * @param {Number} x
 * @param {Number} y
 * @return {Number} direction
 */
function getDirection(x, y) {
    if (x === y) {
        return DIRECTION_NONE;
    }

    if (abs(x) >= abs(y)) {
        return x < 0 ? DIRECTION_LEFT : DIRECTION_RIGHT;
    }
    return y < 0 ? DIRECTION_UP : DIRECTION_DOWN;
}

/**
 * calculate the absolute distance between two points
 * @param {Object} p1 {x, y}
 * @param {Object} p2 {x, y}
 * @param {Array} [props] containing x and y keys
 * @return {Number} distance
 */
function getDistance(p1, p2, props) {
    if (!props) {
        props = PROPS_XY;
    }
    var x = p2[props[0]] - p1[props[0]],
        y = p2[props[1]] - p1[props[1]];

    return Math.hypot(x, y);
}

/**
 * calculate the angle between two coordinates
 * @param {Object} p1
 * @param {Object} p2
 * @param {Array} [props] containing x and y keys
 * @return {Number} angle
 */
function getAngle(p1, p2, props) {
    if (!props) {
        props = PROPS_XY;
    }
    var x = p2[props[0]] - p1[props[0]],
        y = p2[props[1]] - p1[props[1]];
    return Math.atan2(y, x) * 180 / Math.PI;
}

/**
 * calculate the rotation degrees between two pointersets
 * @param {Array} start array of pointers
 * @param {Array} end array of pointers
 * @return {Number} rotation
 */
function getRotation(start, end) {
    return getAngle(end[1], end[0], PROPS_CLIENT_XY) + getAngle(start[1], start[0], PROPS_CLIENT_XY);
}

/**
 * calculate the scale factor between two pointersets
 * no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out
 * @param {Array} start array of pointers
 * @param {Array} end array of pointers
 * @return {Number} scale
 */
function getScale(start, end) {
    return getDistance(end[0], end[1], PROPS_CLIENT_XY) / getDistance(start[0], start[1], PROPS_CLIENT_XY);
}

var MOUSE_INPUT_MAP = {
    mousedown: INPUT_START,
    mousemove: INPUT_MOVE,
    mouseup: INPUT_END
};

var MOUSE_ELEMENT_EVENTS = 'mousedown';
var MOUSE_WINDOW_EVENTS = 'mousemove mouseup';

/**
 * Mouse events input
 * @constructor
 * @extends Input
 */
function MouseInput() {
    this.evEl = MOUSE_ELEMENT_EVENTS;
    this.evWin = MOUSE_WINDOW_EVENTS;

    this.pressed = false; // mousedown state

    Input.apply(this, arguments);
}

inherit(MouseInput, Input, {
    /**
     * handle mouse events
     * @param {Object} ev
     */
    handler: function MEhandler(ev) {
        // console.log('==> MouseInput handler');
        var eventType = MOUSE_INPUT_MAP[ev.type];

        // on start we want to have the left mouse button down
        if (eventType & INPUT_START && ev.button === 0) {
            this.pressed = true;
        }

        if (eventType & INPUT_MOVE && ev.which !== 1) {
            eventType = INPUT_END;
        }

        // mouse must be down
        if (!this.pressed) {
            return;
        }

        if (eventType & INPUT_END) {
            this.pressed = false;
        }

        this.callback(this.manager, eventType, {
            pointers: [ev],
            changedPointers: [ev],
            pointerType: INPUT_TYPE_MOUSE,
            srcEvent: ev
        });
    }
});

var POINTER_INPUT_MAP = {
    pointerdown: INPUT_START,
    pointermove: INPUT_MOVE,
    pointerup: INPUT_END,
    pointercancel: INPUT_CANCEL,
    pointerout: INPUT_CANCEL
};

// in IE10 the pointer types is defined as an enum
var IE10_POINTER_TYPE_ENUM = {
    2: INPUT_TYPE_TOUCH,
    3: INPUT_TYPE_PEN,
    4: INPUT_TYPE_MOUSE,
    5: INPUT_TYPE_KINECT // see https://twitter.com/jacobrossi/status/480596438489890816
};

var POINTER_ELEMENT_EVENTS = 'pointerdown';
var POINTER_WINDOW_EVENTS = 'pointermove pointerup pointercancel';

// IE10 has prefixed support, and case-sensitive
if (window.MSPointerEvent && !window.PointerEvent) {
    POINTER_ELEMENT_EVENTS = 'MSPointerDown';
    POINTER_WINDOW_EVENTS = 'MSPointerMove MSPointerUp MSPointerCancel';
}

/**
 * Pointer events input
 * @constructor
 * @extends Input
 */
function PointerEventInput() {
    this.evEl = POINTER_ELEMENT_EVENTS;
    this.evWin = POINTER_WINDOW_EVENTS;

    Input.apply(this, arguments);

    this.store = (this.manager.session.pointerEvents = []);
}

inherit(PointerEventInput, Input, {
    /**
     * handle mouse events
     * @param {Object} ev
     */
    handler: function PEhandler(ev) {
        // console.log('==> PointerEventInput handler');
        var store = this.store;
        var removePointer = false;

        var eventTypeNormalized = ev.type.toLowerCase().replace('ms', '');
        var eventType = POINTER_INPUT_MAP[eventTypeNormalized];
        var pointerType = IE10_POINTER_TYPE_ENUM[ev.pointerType] || ev.pointerType;

        var isTouch = (pointerType == INPUT_TYPE_TOUCH);

        // get index of the event in the store
        var storeIndex = inArray(store, ev.pointerId, 'pointerId');

        // start and mouse must be down
        if (eventType & INPUT_START && (ev.button === 0 || isTouch)) {
            if (storeIndex < 0) {
                store.push(ev);
                storeIndex = store.length - 1;
            }
        } else if (eventType & (INPUT_END | INPUT_CANCEL)) {
            removePointer = true;
        }

        // it not found, so the pointer hasn't been down (so it's probably a hover)
        if (storeIndex < 0) {
            return;
        }

        // update the event in the store
        store[storeIndex] = ev;

        this.callback(this.manager, eventType, {
            pointers: store,
            changedPointers: [ev],
            pointerType: pointerType,
            srcEvent: ev
        });

        if (removePointer) {
            // remove from the store
            store.splice(storeIndex, 1);
        }
    }
});

var SINGLE_TOUCH_INPUT_MAP = {
    touchstart: INPUT_START,
    touchmove: INPUT_MOVE,
    touchend: INPUT_END,
    touchcancel: INPUT_CANCEL
};

var SINGLE_TOUCH_TARGET_EVENTS = 'touchstart';
var SINGLE_TOUCH_WINDOW_EVENTS = 'touchstart touchmove touchend touchcancel';

/**
 * Touch events input
 * @constructor
 * @extends Input
 */
function SingleTouchInput() {
    this.evTarget = SINGLE_TOUCH_TARGET_EVENTS;
    this.evWin = SINGLE_TOUCH_WINDOW_EVENTS;
    this.started = false;

    Input.apply(this, arguments);
}

inherit(SingleTouchInput, Input, {
    handler: function TEhandler(ev) {
        // console.log('==> SingleTouchInput handler');
        var type = SINGLE_TOUCH_INPUT_MAP[ev.type];

        // should we handle the touch events?
        if (type === INPUT_START) {
            this.started = true;
        }

        if (!this.started) {
            return;
        }

        var touches = normalizeSingleTouches.call(this, ev, type);

        // when done, reset the started state
        if (type & (INPUT_END | INPUT_CANCEL) && touches[0].length - touches[1].length === 0) {
            this.started = false;
        }

        this.callback(this.manager, type, {
            pointers: touches[0],
            changedPointers: touches[1],
            pointerType: INPUT_TYPE_TOUCH,
            srcEvent: ev
        });
    }
});

/**
 * @this {TouchInput}
 * @param {Object} ev
 * @param {Number} type flag
 * @returns {undefined|Array} [all, changed]
 */
function normalizeSingleTouches(ev, type) {
    var all = toArray(ev.touches);
    var changed = toArray(ev.changedTouches);

    if (type & (INPUT_END | INPUT_CANCEL)) {
        all = uniqueArray(all.concat(changed), 'identifier', true);
    }

    return [all, changed];
}

var TOUCH_INPUT_MAP = {
    touchstart: INPUT_START,
    touchmove: INPUT_MOVE,
    touchend: INPUT_END,
    touchcancel: INPUT_CANCEL
};

var TOUCH_TARGET_EVENTS = 'touchstart touchmove touchend touchcancel';

/**
 * Multi-user touch events input
 * @constructor
 * @extends Input
 */
function TouchInput() {
    this.evTarget = TOUCH_TARGET_EVENTS;
    this.targetIds = {};

    Input.apply(this, arguments);
}

inherit(TouchInput, Input, {
    handler: function MTEhandler(ev) {
        // console.log('==> TouchInput handler');
        var type = TOUCH_INPUT_MAP[ev.type];
        var touches = getTouches.call(this, ev, type);
        if (!touches) {
            return;
        }

        this.callback(this.manager, type, {
            pointers: touches[0],
            changedPointers: touches[1],
            pointerType: INPUT_TYPE_TOUCH,
            srcEvent: ev
        });
    }
});

/**
 * @this {TouchInput}
 * @param {Object} ev
 * @param {Number} type flag
 * @returns {undefined|Array} [all, changed]
 */
function getTouches(ev, type) {
    var allTouches = toArray(ev.touches);
    var targetIds = this.targetIds;

    // when there is only one touch, the process can be simplified
    if (type & (INPUT_START | INPUT_MOVE) && allTouches.length === 1) {
        targetIds[allTouches[0].identifier] = true;
        return [allTouches, allTouches];
    }

    var i,
        targetTouches,
        changedTouches = toArray(ev.changedTouches),
        changedTargetTouches = [],
        target = this.target;

    // get target touches from touches
    targetTouches = allTouches.filter(function(touch) {
        return hasParent(touch.target, target);
    });

    // collect touches
    if (type === INPUT_START) {
        i = 0;
        while (i < targetTouches.length) {
            targetIds[targetTouches[i].identifier] = true;
            i++;
        }
    }

    // filter changed touches to only contain touches that exist in the collected target ids
    i = 0;
    while (i < changedTouches.length) {
        if (targetIds[changedTouches[i].identifier]) {
            changedTargetTouches.push(changedTouches[i]);
        }

        // cleanup removed touches
        if (type & (INPUT_END | INPUT_CANCEL)) {
            delete targetIds[changedTouches[i].identifier];
        }
        i++;
    }

    if (!changedTargetTouches.length) {
        return;
    }

    return [
        // merge targetTouches with changedTargetTouches so it contains ALL touches, including 'end' and 'cancel'
        uniqueArray(targetTouches.concat(changedTargetTouches), 'identifier', true),
        changedTargetTouches
    ];
}

/**
 * Combined touch and mouse input
 *
 * Touch has a higher priority then mouse, and while touching no mouse events are allowed.
 * This because touch devices also emit mouse events while doing a touch.
 *
 * @constructor
 * @extends Input
 */

var DEDUP_TIMEOUT = 2500;
var DEDUP_DISTANCE = 25;

function TouchMouseInput() {
    Input.apply(this, arguments);

    var handler = bindFn(this.handler, this);
    this.touch = new TouchInput(this.manager, handler);
    this.mouse = new MouseInput(this.manager, handler);

    this.primaryTouch = null;
    this.lastTouches = [];
}

inherit(TouchMouseInput, Input, {
    /**
     * handle mouse and touch events
     * @param {Hammer} manager
     * @param {String} inputEvent
     * @param {Object} inputData
     */
    handler: function TMEhandler(manager, inputEvent, inputData) {
        // console.log('==> TouchMouseInput handler');
        var isTouch = (inputData.pointerType == INPUT_TYPE_TOUCH),
            isMouse = (inputData.pointerType == INPUT_TYPE_MOUSE);

        if (isMouse && inputData.sourceCapabilities && inputData.sourceCapabilities.firesTouchEvents) {
            return;
        }

        // when we're in a touch event, record touches to  de-dupe synthetic mouse event
        if (isTouch) {
            recordTouches.call(this, inputEvent, inputData);
        } else if (isMouse && isSyntheticEvent.call(this, inputData)) {
            return;
        }

        this.callback(manager, inputEvent, inputData);
    },

    /**
     * remove the event listeners
     */
    destroy: function destroy() {
        this.touch.destroy();
        this.mouse.destroy();
    }
});

function recordTouches(eventType, eventData) {
    if (eventType & INPUT_START) {
        this.primaryTouch = eventData.changedPointers[0].identifier;
        setLastTouch.call(this, eventData);
    } else if (eventType & (INPUT_END | INPUT_CANCEL)) {
        setLastTouch.call(this, eventData);
    }
}

function setLastTouch(eventData) {
    var touch = eventData.changedPointers[0];

    if (touch.identifier === this.primaryTouch) {
        var lastTouch = {x: touch.clientX, y: touch.clientY};
        this.lastTouches.push(lastTouch);
        var lts = this.lastTouches;
        var removeLastTouch = function() {
            var i = lts.indexOf(lastTouch);
            if (i > -1) {
                lts.splice(i, 1);
            }
        };
        setTimeout(removeLastTouch, DEDUP_TIMEOUT);
    }
}

function isSyntheticEvent(eventData) {
    var x = eventData.srcEvent.clientX, y = eventData.srcEvent.clientY;
    for (var i = 0; i < this.lastTouches.length; i++) {
        var t = this.lastTouches[i];
        var dx = Math.abs(x - t.x), dy = Math.abs(y - t.y);
        if (dx <= DEDUP_DISTANCE && dy <= DEDUP_DISTANCE) {
            return true;
        }
    }
    return false;
}

var PREFIXED_TOUCH_ACTION = prefixed(TEST_ELEMENT.style, 'touchAction');
var NATIVE_TOUCH_ACTION = PREFIXED_TOUCH_ACTION !== undefined;

// magical touchAction value
var TOUCH_ACTION_COMPUTE = 'compute';
var TOUCH_ACTION_AUTO = 'auto';
var TOUCH_ACTION_MANIPULATION = 'manipulation'; // not implemented
var TOUCH_ACTION_NONE = 'none';
var TOUCH_ACTION_PAN_X = 'pan-x';
var TOUCH_ACTION_PAN_Y = 'pan-y';
var TOUCH_ACTION_MAP = getTouchActionProps();

/**
 * Touch Action
 * sets the touchAction property or uses the js alternative
 * @param {Manager} manager
 * @param {String} value
 * @constructor
 */
function TouchAction(manager, value) {
    this.manager = manager;
    this.set(value);
}

TouchAction.prototype = {
    /**
     * set the touchAction value on the element or enable the polyfill
     * @param {String} value
     */
    set: function(value) {
        // find out the touch-action by the event handlers
        if (value == TOUCH_ACTION_COMPUTE) {
            value = this.compute();
        }

        if (NATIVE_TOUCH_ACTION && this.manager.element.style && TOUCH_ACTION_MAP[value]) {
            this.manager.element.style[PREFIXED_TOUCH_ACTION] = value;
        }
        this.actions = value.toLowerCase().trim();
    },

    /**
     * just re-set the touchAction value
     */
    update: function() {
        this.set(this.manager.options.touchAction);
    },

    /**
     * compute the value for the touchAction property based on the recognizer's settings
     * @returns {String} value
     */
    compute: function() {
        var actions = [];
        each(this.manager.recognizers, function(recognizer) {
            if (boolOrFn(recognizer.options.enable, [recognizer])) {
                actions = actions.concat(recognizer.getTouchAction());
            }
        });
        return cleanTouchActions(actions.join(' '));
    },

    /**
     * this method is called on each input cycle and provides the preventing of the browser behavior
     * @param {Object} input
     */
    preventDefaults: function(input) {
        var srcEvent = input.srcEvent;
        var direction = input.offsetDirection;

        // if the touch action did prevented once this session
        if (this.manager.session.prevented) {
            srcEvent.preventDefault();
            return;
        }

        var actions = this.actions;
        var hasNone = inStr(actions, TOUCH_ACTION_NONE) && !TOUCH_ACTION_MAP[TOUCH_ACTION_NONE];
        var hasPanY = inStr(actions, TOUCH_ACTION_PAN_Y) && !TOUCH_ACTION_MAP[TOUCH_ACTION_PAN_Y];
        var hasPanX = inStr(actions, TOUCH_ACTION_PAN_X) && !TOUCH_ACTION_MAP[TOUCH_ACTION_PAN_X];

        if (hasNone) {
            //do not prevent defaults if this is a tap gesture

            var isTapPointer = input.pointers.length === 1;
            var isTapMovement = input.distance < 2;
            var isTapTouchTime = input.deltaTime < 250;

            if (isTapPointer && isTapMovement && isTapTouchTime) {
                return;
            }
        }

        if (hasPanX && hasPanY) {
            // `pan-x pan-y` means browser handles all scrolling/panning, do not prevent
            return;
        }

        if (hasNone ||
            (hasPanY && direction & DIRECTION_HORIZONTAL) ||
            (hasPanX && direction & DIRECTION_VERTICAL)) {
            return this.preventSrc(srcEvent);
        }
    },

    /**
     * call preventDefault to prevent the browser's default behavior (scrolling in most cases)
     * @param {Object} srcEvent
     */
    preventSrc: function(srcEvent) {
        this.manager.session.prevented = true;
        srcEvent.preventDefault();
    }
};

/**
 * when the touchActions are collected they are not a valid value, so we need to clean things up. *
 * @param {String} actions
 * @returns {*}
 */
function cleanTouchActions(actions) {
    // none
    if (inStr(actions, TOUCH_ACTION_NONE)) {
        return TOUCH_ACTION_NONE;
    }

    var hasPanX = inStr(actions, TOUCH_ACTION_PAN_X);
    var hasPanY = inStr(actions, TOUCH_ACTION_PAN_Y);

    // if both pan-x and pan-y are set (different recognizers
    // for different directions, e.g. horizontal pan but vertical swipe?)
    // we need none (as otherwise with pan-x pan-y combined none of these
    // recognizers will work, since the browser would handle all panning
    if (hasPanX && hasPanY) {
        return TOUCH_ACTION_NONE;
    }

    // pan-x OR pan-y
    if (hasPanX || hasPanY) {
        return hasPanX ? TOUCH_ACTION_PAN_X : TOUCH_ACTION_PAN_Y;
    }

    // manipulation
    if (inStr(actions, TOUCH_ACTION_MANIPULATION)) {
        return TOUCH_ACTION_MANIPULATION;
    }

    return TOUCH_ACTION_AUTO;
}

function getTouchActionProps() {
    if (!NATIVE_TOUCH_ACTION) {
        return false;
    }
    var touchMap = {};
    var cssSupports = window.CSS && window.CSS.supports;
    ['auto', 'manipulation', 'pan-y', 'pan-x', 'pan-x pan-y', 'none'].forEach(function(val) {

        // If css.supports is not supported but there is native touch-action assume it supports
        // all values. This is the case for IE 10 and 11.
        touchMap[val] = cssSupports ? window.CSS.supports('touch-action', val) : true;
    });
    return touchMap;
}

/**
 * Recognizer flow explained; *
 * All recognizers have the initial state of POSSIBLE when an input session starts.
 * The definition of an input session is from the first input until the last input, with all it's movement in it. *
 * Example session for mouse-input: mousedown -> mousemove -> mouseup
 *
 * On each recognizing cycle (see Manager.recognize) the .recognize() method is executed
 * which determines with state it should be.
 *
 * If the recognizer has the state FAILED, CANCELLED or RECOGNIZED (equals ENDED), it is reset to
 * POSSIBLE to give it another change on the next cycle.
 *
 *               Possible
 *                  |
 *            +-----+---------------+
 *            |                     |
 *      +-----+-----+               |
 *      |           |               |
 *   Failed      Cancelled          |
 *                          +-------+------+
 *                          |              |
 *                      Recognized       Began
 *                                         |
 *                                      Changed
 *                                         |
 *                                  Ended/Recognized
 */
var STATE_POSSIBLE = 1;
var STATE_BEGAN = 2;
var STATE_CHANGED = 4;
var STATE_ENDED = 8;
var STATE_RECOGNIZED = STATE_ENDED;
var STATE_CANCELLED = 16;
var STATE_FAILED = 32;

/**
 * Recognizer
 * Every recognizer needs to extend from this class.
 * @constructor
 * @param {Object} options
 */
function Recognizer(options) {
    this.options = assign({}, this.defaults, options || {});

    this.id = uniqueId();

    this.manager = null;

    // default is enable true
    this.options.enable = ifUndefined(this.options.enable, true);

    this.state = STATE_POSSIBLE;

    this.simultaneous = {};
    this.requireFail = [];
}

Recognizer.prototype = {
    /**
     * @virtual
     * @type {Object}
     */
    defaults: {},

    /**
     * set options
     * @param {Object} options
     * @return {Recognizer}
     */
    set: function(options) {
        assign(this.options, options);

        // also update the touchAction, in case something changed about the directions/enabled state
        this.manager && this.manager.touchAction.update();
        return this;
    },

    /**
     * recognize simultaneous with another recognizer.
     * @param {Recognizer} otherRecognizer
     * @returns {Recognizer} this
     */
    recognizeWith: function(otherRecognizer) {
        if (invokeArrayArg(otherRecognizer, 'recognizeWith', this)) {
            return this;
        }

        var simultaneous = this.simultaneous;
        otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this);
        if (!simultaneous[otherRecognizer.id]) {
            simultaneous[otherRecognizer.id] = otherRecognizer;
            otherRecognizer.recognizeWith(this);
        }
        return this;
    },

    /**
     * drop the simultaneous link. It doesn't remove the link on the other recognizer.
     * @param {Recognizer} otherRecognizer
     * @returns {Recognizer} this
     */
    dropRecognizeWith: function(otherRecognizer) {
        if (invokeArrayArg(otherRecognizer, 'dropRecognizeWith', this)) {
            return this;
        }

        otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this);
        delete this.simultaneous[otherRecognizer.id];
        return this;
    },

    /**
     * recognizer can only run when another is failing
     * @param {Recognizer} otherRecognizer
     * @returns {Recognizer} this
     */
    requireFailure: function(otherRecognizer) {
        if (invokeArrayArg(otherRecognizer, 'requireFailure', this)) {
            return this;
        }

        var requireFail = this.requireFail;
        otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this);
        if (inArray(requireFail, otherRecognizer) === -1) {
            requireFail.push(otherRecognizer);
            otherRecognizer.requireFailure(this);
        }
        return this;
    },

    /**
     * drop the requireFailure link. It does not remove the link on the other recognizer.
     * @param {Recognizer} otherRecognizer
     * @returns {Recognizer} this
     */
    dropRequireFailure: function(otherRecognizer) {
        if (invokeArrayArg(otherRecognizer, 'dropRequireFailure', this)) {
            return this;
        }

        otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this);
        var index = inArray(this.requireFail, otherRecognizer);
        if (index > -1) {
            this.requireFail.splice(index, 1);
        }
        return this;
    },

    /**
     * has require failures boolean
     * @returns {boolean}
     */
    hasRequireFailures: function() {
        return this.requireFail.length > 0;
    },

    /**
     * if the recognizer can recognize simultaneous with another recognizer
     * @param {Recognizer} otherRecognizer
     * @returns {Boolean}
     */
    canRecognizeWith: function(otherRecognizer) {
        return !!this.simultaneous[otherRecognizer.id];
    },

    /**
     * You should use `tryEmit` instead of `emit` directly to check
     * that all the needed recognizers has failed before emitting.
     * @param {Object} input
     */
    emit: function(input) {
        var self = this;
        var state = this.state;

        function emit(event) {
            self.manager.emit(event, input);
        }

        // 'panstart' and 'panmove'
        if (state < STATE_ENDED) {
            emit(self.options.event + stateStr(state));
        }

        emit(self.options.event); // simple 'eventName' events

        if (input.additionalEvent) { // additional event(panleft, panright, pinchin, pinchout...)
            emit(input.additionalEvent);
        }

        // panend and pancancel
        if (state >= STATE_ENDED) {
            emit(self.options.event + stateStr(state));
        }
    },

    /**
     * Check that all the require failure recognizers has failed,
     * if true, it emits a gesture event,
     * otherwise, setup the state to FAILED.
     * @param {Object} input
     */
    tryEmit: function(input) {
        if (this.canEmit()) {
            return this.emit(input);
        }
        // it's failing anyway
        this.state = STATE_FAILED;
    },

    /**
     * can we emit?
     * @returns {boolean}
     */
    canEmit: function() {
        var i = 0;
        while (i < this.requireFail.length) {
            if (!(this.requireFail[i].state & (STATE_FAILED | STATE_POSSIBLE))) {
                return false;
            }
            i++;
        }
        return true;
    },

    /**
     * update the recognizer
     * @param {Object} inputData
     */
    recognize: function(inputData) {
        // make a new copy of the inputData
        // so we can change the inputData without messing up the other recognizers
        var inputDataClone = assign({}, inputData);

        // is it enabled and allow recognizing?
        if (!boolOrFn(this.options.enable, [this, inputDataClone])) {
            this.reset();
            this.state = STATE_FAILED;
            return;
        }

        // reset when we've reached the end
        if (this.state & (STATE_RECOGNIZED | STATE_CANCELLED | STATE_FAILED)) {
            this.state = STATE_POSSIBLE;
        }

        this.state = this.process(inputDataClone);

        // the recognizer has recognized a gesture
        // so trigger an event
        if (this.state & (STATE_BEGAN | STATE_CHANGED | STATE_ENDED | STATE_CANCELLED)) {
            this.tryEmit(inputDataClone);
        }
    },

    /**
     * return the state of the recognizer
     * the actual recognizing happens in this method
     * @virtual
     * @param {Object} inputData
     * @returns {Const} STATE
     */
    process: function(inputData) { }, // jshint ignore:line

    /**
     * return the preferred touch-action
     * @virtual
     * @returns {Array}
     */
    getTouchAction: function() { },

    /**
     * called when the gesture isn't allowed to recognize
     * like when another is being recognized or it is disabled
     * @virtual
     */
    reset: function() { }
};

/**
 * get a usable string, used as event postfix
 * @param {Const} state
 * @returns {String} state
 */
function stateStr(state) {
    if (state & STATE_CANCELLED) {
        return 'cancel';
    } else if (state & STATE_ENDED) {
        return 'end';
    } else if (state & STATE_CHANGED) {
        return 'move';
    } else if (state & STATE_BEGAN) {
        return 'start';
    }
    return '';
}

/**
 * direction cons to string
 * @param {Const} direction
 * @returns {String}
 */
function directionStr(direction) {
    if (direction == DIRECTION_DOWN) {
        return 'down';
    } else if (direction == DIRECTION_UP) {
        return 'up';
    } else if (direction == DIRECTION_LEFT) {
        return 'left';
    } else if (direction == DIRECTION_RIGHT) {
        return 'right';
    }
    return '';
}

/**
 * get a recognizer by name if it is bound to a manager
 * @param {Recognizer|String} otherRecognizer
 * @param {Recognizer} recognizer
 * @returns {Recognizer}
 */
function getRecognizerByNameIfManager(otherRecognizer, recognizer) {
    var manager = recognizer.manager;
    if (manager) {
        return manager.get(otherRecognizer);
    }
    return otherRecognizer;
}

/**
 * This recognizer is just used as a base for the simple attribute recognizers.
 * @constructor
 * @extends Recognizer
 */
function AttrRecognizer() {
    Recognizer.apply(this, arguments);
}

inherit(AttrRecognizer, Recognizer, {
    /**
     * @namespace
     * @memberof AttrRecognizer
     */
    defaults: {
        /**
         * @type {Number}
         * @default 1
         */
        pointers: 1
    },

    /**
     * Used to check if it the recognizer receives valid input, like input.distance > 10.
     * @memberof AttrRecognizer
     * @param {Object} input
     * @returns {Boolean} recognized
     */
    attrTest: function(input) {
        var optionPointers = this.options.pointers;
        return optionPointers === 0 || input.pointers.length === optionPointers;
    },

    /**
     * Process the input and return the state for the recognizer
     * @memberof AttrRecognizer
     * @param {Object} input
     * @returns {*} State
     */
    process: function(input) {
        var state = this.state;
        var eventType = input.eventType;

        var isRecognized = state & (STATE_BEGAN | STATE_CHANGED);
        var isValid = this.attrTest(input);

        // on cancel input and we've recognized before, return STATE_CANCELLED
        if (isRecognized && (eventType & INPUT_CANCEL || !isValid)) {
            return state | STATE_CANCELLED;
        } else if (isRecognized || isValid) {
            if (eventType & INPUT_END) {
                return state | STATE_ENDED;
            } else if (!(state & STATE_BEGAN)) {
                return STATE_BEGAN;
            }
            return state | STATE_CHANGED;
        }
        return STATE_FAILED;
    }
});

/**
 * Pan
 * Recognized when the pointer is down and moved in the allowed direction.
 * @constructor
 * @extends AttrRecognizer
 */
function PanRecognizer() {
    AttrRecognizer.apply(this, arguments);

    this.pX = null;
    this.pY = null;
}

inherit(PanRecognizer, AttrRecognizer, {
    /**
     * @namespace
     * @memberof PanRecognizer
     */
    defaults: {
        event: 'pan',
        threshold: 10,
        pointers: 1,
        direction: DIRECTION_ALL
    },

    getTouchAction: function() {
        var direction = this.options.direction;
        var actions = [];
        if (direction & DIRECTION_HORIZONTAL) {
            actions.push(TOUCH_ACTION_PAN_Y);
        }
        if (direction & DIRECTION_VERTICAL) {
            actions.push(TOUCH_ACTION_PAN_X);
        }
        return actions;
    },

    directionTest: function(input) {
        var options = this.options;
        var hasMoved = true;
        var distance = input.distance;
        var direction = input.direction;
        var x = input.deltaX;
        var y = input.deltaY;

        // lock to axis?
        if (!(direction & options.direction)) {
            if (options.direction & DIRECTION_HORIZONTAL) {
                direction = (x === 0) ? DIRECTION_NONE : (x < 0) ? DIRECTION_LEFT : DIRECTION_RIGHT;
                hasMoved = x != this.pX;
                distance = Math.abs(input.deltaX);
            } else {
                direction = (y === 0) ? DIRECTION_NONE : (y < 0) ? DIRECTION_UP : DIRECTION_DOWN;
                hasMoved = y != this.pY;
                distance = Math.abs(input.deltaY);
            }
        }
        input.direction = direction;
        return hasMoved && distance > options.threshold && direction & options.direction;
    },

    attrTest: function(input) {
        return AttrRecognizer.prototype.attrTest.call(this, input) &&
            (this.state & STATE_BEGAN || (!(this.state & STATE_BEGAN) && this.directionTest(input)));
    },

    emit: function(input) {

        this.pX = input.deltaX;
        this.pY = input.deltaY;

        var direction = directionStr(input.direction);

        if (direction) {
            input.additionalEvent = this.options.event + direction;
        }
        this._super.emit.call(this, input);
    }
});

/**
 * Pinch
 * Recognized when two or more pointers are moving toward (zoom-in) or away from each other (zoom-out).
 * @constructor
 * @extends AttrRecognizer
 */
function PinchRecognizer() {
    AttrRecognizer.apply(this, arguments);
}

inherit(PinchRecognizer, AttrRecognizer, {
    /**
     * @namespace
     * @memberof PinchRecognizer
     */
    defaults: {
        event: 'pinch',
        threshold: 0,
        pointers: 2
    },

    getTouchAction: function() {
        return [TOUCH_ACTION_NONE];
    },

    attrTest: function(input) {
        return this._super.attrTest.call(this, input) &&
            (Math.abs(input.scale - 1) > this.options.threshold || this.state & STATE_BEGAN);
    },

    emit: function(input) {
        if (input.scale !== 1) {
            var inOut = input.scale < 1 ? 'in' : 'out';
            input.additionalEvent = this.options.event + inOut;
        }
        this._super.emit.call(this, input);
    }
});

/**
 * Press
 * Recognized when the pointer is down for x ms without any movement.
 * @constructor
 * @extends Recognizer
 */
function PressRecognizer() {
    Recognizer.apply(this, arguments);

    this._timer = null;
    this._input = null;
}

inherit(PressRecognizer, Recognizer, {
    /**
     * @namespace
     * @memberof PressRecognizer
     */
    defaults: {
        event: 'press',
        pointers: 1,
        time: 251, // minimal time of the pointer to be pressed
        threshold: 9 // a minimal movement is ok, but keep it low
    },

    getTouchAction: function() {
        return [TOUCH_ACTION_AUTO];
    },

    process: function(input) {
        var options = this.options;
        var validPointers = input.pointers.length === options.pointers;
        var validMovement = input.distance < options.threshold;
        var validTime = input.deltaTime > options.time;

        this._input = input;

        // we only allow little movement
        // and we've reached an end event, so a tap is possible
        if (!validMovement || !validPointers || (input.eventType & (INPUT_END | INPUT_CANCEL) && !validTime)) {
            this.reset();
        } else if (input.eventType & INPUT_START) {
            this.reset();
            this._timer = setTimeoutContext(function() {
                this.state = STATE_RECOGNIZED;
                this.tryEmit();
            }, options.time, this);
        } else if (input.eventType & INPUT_END) {
            return STATE_RECOGNIZED;
        }
        return STATE_FAILED;
    },

    reset: function() {
        clearTimeout(this._timer);
    },

    emit: function(input) {
        if (this.state !== STATE_RECOGNIZED) {
            return;
        }

        if (input && (input.eventType & INPUT_END)) {
            this.manager.emit(this.options.event + 'up', input);
        } else {
            this._input.timeStamp = now();
            this.manager.emit(this.options.event, this._input);
        }
    }
});

/**
 * Rotate
 * Recognized when two or more pointer are moving in a circular motion.
 * @constructor
 * @extends AttrRecognizer
 */
function RotateRecognizer() {
    AttrRecognizer.apply(this, arguments);
}

inherit(RotateRecognizer, AttrRecognizer, {
    /**
     * @namespace
     * @memberof RotateRecognizer
     */
    defaults: {
        event: 'rotate',
        threshold: 0,
        pointers: 2
    },

    getTouchAction: function() {
        return [TOUCH_ACTION_NONE];
    },

    attrTest: function(input) {
        return this._super.attrTest.call(this, input) &&
            (Math.abs(input.rotation) > this.options.threshold || this.state & STATE_BEGAN);
    }
});

/**
 * Swipe
 * Recognized when the pointer is moving fast (velocity), with enough distance in the allowed direction.
 * @constructor
 * @extends AttrRecognizer
 */
function SwipeRecognizer() {
    AttrRecognizer.apply(this, arguments);
}

inherit(SwipeRecognizer, AttrRecognizer, {
    /**
     * @namespace
     * @memberof SwipeRecognizer
     */
    defaults: {
        event: 'swipe',
        threshold: 10,
        velocity: 0.3,
        direction: DIRECTION_HORIZONTAL | DIRECTION_VERTICAL,
        pointers: 1
    },

    getTouchAction: function() {
        return PanRecognizer.prototype.getTouchAction.call(this);
    },

    attrTest: function(input) {
        var direction = this.options.direction;
        var velocity;

        if (direction & (DIRECTION_HORIZONTAL | DIRECTION_VERTICAL)) {
            velocity = input.overallVelocity;
        } else if (direction & DIRECTION_HORIZONTAL) {
            velocity = input.overallVelocityX;
        } else if (direction & DIRECTION_VERTICAL) {
            velocity = input.overallVelocityY;
        }

        return this._super.attrTest.call(this, input) &&
            direction & input.offsetDirection &&
            input.distance > this.options.threshold &&
            input.maxPointers == this.options.pointers &&
            abs(velocity) > this.options.velocity && input.eventType & INPUT_END;
    },

    emit: function(input) {
        var direction = directionStr(input.offsetDirection);
        if (direction) {
            this.manager.emit(this.options.event + direction, input);
        }

        this.manager.emit(this.options.event, input);
    }
});

/**
 * A tap is recognized when the pointer is doing a small tap/click. Multiple taps are recognized if they occur
 * between the given interval and position. The delay option can be used to recognize multi-taps without firing
 * a single tap.
 *
 * The eventData from the emitted event contains the property `tapCount`, which contains the amount of
 * multi-taps being recognized.
 * @constructor
 * @extends Recognizer
 */
function TapRecognizer() {
    Recognizer.apply(this, arguments);

    // previous time and center,
    // used for tap counting
    this.pTime = false;
    this.pCenter = false;

    this._timer = null;
    this._input = null;
    this.count = 0;
}

inherit(TapRecognizer, Recognizer, {
    /**
     * @namespace
     * @memberof PinchRecognizer
     */
    defaults: {
        event: 'tap',
        pointers: 1,
        taps: 1,
        interval: 300, // max time between the multi-tap taps
        time: 250, // max time of the pointer to be down (like finger on the screen)
        threshold: 9, // a minimal movement is ok, but keep it low
        posThreshold: 10 // a multi-tap can be a bit off the initial position
    },

    getTouchAction: function() {
        return [TOUCH_ACTION_MANIPULATION];
    },

    process: function(input) {
        var options = this.options;

        var validPointers = input.pointers.length === options.pointers;
        var validMovement = input.distance < options.threshold;
        var validTouchTime = input.deltaTime < options.time;

        this.reset();

        if ((input.eventType & INPUT_START) && (this.count === 0)) {
            return this.failTimeout();
        }

        // we only allow little movement
        // and we've reached an end event, so a tap is possible
        if (validMovement && validTouchTime && validPointers) {
            if (input.eventType != INPUT_END) {
                return this.failTimeout();
            }

            var validInterval = this.pTime ? (input.timeStamp - this.pTime < options.interval) : true;
            var validMultiTap = !this.pCenter || getDistance(this.pCenter, input.center) < options.posThreshold;

            this.pTime = input.timeStamp;
            this.pCenter = input.center;

            if (!validMultiTap || !validInterval) {
                this.count = 1;
            } else {
                this.count += 1;
            }

            this._input = input;

            // if tap count matches we have recognized it,
            // else it has begun recognizing...
            var tapCount = this.count % options.taps;
            if (tapCount === 0) {
                // no failing requirements, immediately trigger the tap event
                // or wait as long as the multitap interval to trigger
                if (!this.hasRequireFailures()) {
                    return STATE_RECOGNIZED;
                } else {
                    this._timer = setTimeoutContext(function() {
                        this.state = STATE_RECOGNIZED;
                        this.tryEmit();
                    }, options.interval, this);
                    return STATE_BEGAN;
                }
            }
        }
        return STATE_FAILED;
    },

    failTimeout: function() {
        this._timer = setTimeoutContext(function() {
            this.state = STATE_FAILED;
        }, this.options.interval, this);
        return STATE_FAILED;
    },

    reset: function() {
        clearTimeout(this._timer);
    },

    emit: function() {
        if (this.state == STATE_RECOGNIZED) {
            this._input.tapCount = this.count;
            this.manager.emit(this.options.event, this._input);
        }
    }
});

/**
 * Simple way to create a manager with a default set of recognizers.
 * @param {HTMLElement} element
 * @param {Object} [options]
 * @constructor
 */
function Hammer(element, options) {
    options = options || {};
    options.recognizers = ifUndefined(options.recognizers, Hammer.defaults.preset);
    return new Manager(element, options);
}

/**
 * @const {string}
 */
Hammer.VERSION = '2.0.7';

/**
 * default settings
 * @namespace
 */
Hammer.defaults = {
    /**
     * set if DOM events are being triggered.
     * But this is slower and unused by simple implementations, so disabled by default.
     * @type {Boolean}
     * @default false
     */
    domEvents: false,

    /**
     * The value for the touchAction property/fallback.
     * When set to `compute` it will magically set the correct value based on the added recognizers.
     * @type {String}
     * @default compute
     */
    touchAction: TOUCH_ACTION_COMPUTE,

    /**
     * @type {Boolean}
     * @default true
     */
    enable: true,

    /**
     * EXPERIMENTAL FEATURE -- can be removed/changed
     * Change the parent input target element.
     * If Null, then it is being set the to main element.
     * @type {Null|EventTarget}
     * @default null
     */
    inputTarget: null,

    /**
     * force an input class
     * @type {Null|Function}
     * @default null
     */
    inputClass: null,

    /**
     * Default recognizer setup when calling `Hammer()`
     * When creating a new Manager these will be skipped.
     * @type {Array}
     */
    preset: [
        // RecognizerClass, options, [recognizeWith, ...], [requireFailure, ...]
        [RotateRecognizer, {enable: false}],
        [PinchRecognizer, {enable: false}, ['rotate']],
        [SwipeRecognizer, {direction: DIRECTION_HORIZONTAL}],
        [PanRecognizer, {direction: DIRECTION_HORIZONTAL}, ['swipe']],
        [TapRecognizer],
        [TapRecognizer, {event: 'doubletap', taps: 2}, ['tap']],
        [PressRecognizer]
    ],

    /**
     * Some CSS properties can be used to improve the working of Hammer.
     * Add them to this method and they will be set when creating a new Manager.
     * @namespace
     */
    cssProps: {
        /**
         * Disables text selection to improve the dragging gesture. Mainly for desktop browsers.
         * @type {String}
         * @default 'none'
         */
        userSelect: 'none',

        /**
         * Disable the Windows Phone grippers when pressing an element.
         * @type {String}
         * @default 'none'
         */
        touchSelect: 'none',

        /**
         * Disables the default callout shown when you touch and hold a touch target.
         * On iOS, when you touch and hold a touch target such as a link, Safari displays
         * a callout containing information about the link. This property allows you to disable that callout.
         * @type {String}
         * @default 'none'
         */
        touchCallout: 'none',

        /**
         * Specifies whether zooming is enabled. Used by IE10>
         * @type {String}
         * @default 'none'
         */
        contentZooming: 'none',

        /**
         * Specifies that an entire element should be draggable instead of its contents. Mainly for desktop browsers.
         * @type {String}
         * @default 'none'
         */
        userDrag: 'none',

        /**
         * Overrides the highlight color shown when the user taps a link or a JavaScript
         * clickable element in iOS. This property obeys the alpha value, if specified.
         * @type {String}
         * @default 'rgba(0,0,0,0)'
         */
        tapHighlightColor: 'rgba(0,0,0,0)'
    }
};

var STOP = 1;
var FORCED_STOP = 2;

/**
 * Manager
 * @param {HTMLElement} element
 * @param {Object} [options]
 * @constructor
 */
function Manager(element, options) {
    this.options = assign({}, Hammer.defaults, options || {});

    this.options.inputTarget = this.options.inputTarget || element;

    this.handlers = {};
    this.session = {};
    this.recognizers = [];
    this.oldCssProps = {};

    this.element = element;
    this.input = createInputInstance(this);
    this.touchAction = new TouchAction(this, this.options.touchAction);

    toggleCssProps(this, true);

    each(this.options.recognizers, function(item) {
        var recognizer = this.add(new (item[0])(item[1]));
        item[2] && recognizer.recognizeWith(item[2]);
        item[3] && recognizer.requireFailure(item[3]);
    }, this);
}

Manager.prototype = {
    /**
     * set options
     * @param {Object} options
     * @returns {Manager}
     */
    set: function(options) {
        assign(this.options, options);

        // Options that need a little more setup
        if (options.touchAction) {
            this.touchAction.update();
        }
        if (options.inputTarget) {
            // Clean up existing event listeners and reinitialize
            this.input.destroy();
            this.input.target = options.inputTarget;
            this.input.init();
        }
        return this;
    },

    /**
     * stop recognizing for this session.
     * This session will be discarded, when a new [input]start event is fired.
     * When forced, the recognizer cycle is stopped immediately.
     * @param {Boolean} [force]
     */
    stop: function(force) {
        this.session.stopped = force ? FORCED_STOP : STOP;
    },

    /**
     * run the recognizers!
     * called by the inputHandler function on every movement of the pointers (touches)
     * it walks through all the recognizers and tries to detect the gesture that is being made
     * @param {Object} inputData
     */
    recognize: function(inputData) {
        var session = this.session;
        if (session.stopped) {
            return;
        }

        // run the touch-action polyfill
        this.touchAction.preventDefaults(inputData);

        var recognizer;
        var recognizers = this.recognizers;

        // this holds the recognizer that is being recognized.
        // so the recognizer's state needs to be BEGAN, CHANGED, ENDED or RECOGNIZED
        // if no recognizer is detecting a thing, it is set to `null`
        var curRecognizer = session.curRecognizer;

        // reset when the last recognizer is recognized
        // or when we're in a new session
        if (!curRecognizer || (curRecognizer && curRecognizer.state & STATE_RECOGNIZED)) {
            curRecognizer = session.curRecognizer = null;
        }

        var i = 0;
        while (i < recognizers.length) {
            recognizer = recognizers[i];

            // find out if we are allowed try to recognize the input for this one.
            // 1.   allow if the session is NOT forced stopped (see the .stop() method)
            // 2.   allow if we still haven't recognized a gesture in this session, or the this recognizer is the one
            //      that is being recognized.
            // 3.   allow if the recognizer is allowed to run simultaneous with the current recognized recognizer.
            //      this can be setup with the `recognizeWith()` method on the recognizer.
            if (session.stopped !== FORCED_STOP && ( // 1
                    !curRecognizer || recognizer == curRecognizer || // 2
                    recognizer.canRecognizeWith(curRecognizer))) { // 3
                recognizer.recognize(inputData);
            } else {
                recognizer.reset();
            }

            // if the recognizer has been recognizing the input as a valid gesture, we want to store this one as the
            // current active recognizer. but only if we don't already have an active recognizer
            if (!curRecognizer && recognizer.state & (STATE_BEGAN | STATE_CHANGED | STATE_ENDED)) {
                curRecognizer = session.curRecognizer = recognizer;
            }
            i++;
        }
    },

    /**
     * get a recognizer by its event name.
     * @param {Recognizer|String} recognizer
     * @returns {Recognizer|Null}
     */
    get: function(recognizer) {
        if (recognizer instanceof Recognizer) {
            return recognizer;
        }

        var recognizers = this.recognizers;
        for (var i = 0; i < recognizers.length; i++) {
            if (recognizers[i].options.event == recognizer) {
                return recognizers[i];
            }
        }
        return null;
    },

    /**
     * add a recognizer to the manager
     * existing recognizers with the same event name will be removed
     * @param {Recognizer} recognizer
     * @returns {Recognizer|Manager}
     */
    add: function(recognizer) {
        if (invokeArrayArg(recognizer, 'add', this)) {
            return this;
        }

        // remove existing
        var existing = this.get(recognizer.options.event);
        if (existing) {
            this.remove(existing);
        }

        this.recognizers.push(recognizer);
        recognizer.manager = this;

        this.touchAction.update();
        return recognizer;
    },

    /**
     * remove a recognizer by name or instance
     * @param {Recognizer|String} recognizer
     * @returns {Manager}
     */
    remove: function(recognizer) {
        if (invokeArrayArg(recognizer, 'remove', this)) {
            return this;
        }

        recognizer = this.get(recognizer);

        // let's make sure this recognizer exists
        if (recognizer) {
            var recognizers = this.recognizers;
            var index = inArray(recognizers, recognizer);

            if (index !== -1) {
                recognizers.splice(index, 1);
                this.touchAction.update();
            }
        }

        return this;
    },

    /**
     * bind event
     * @param {String} events
     * @param {Function} handler
     * @returns {EventEmitter} this
     */
    on: function(events, handler) {
        if (events === undefined) {
            return;
        }
        if (handler === undefined) {
            return;
        }

        var handlers = this.handlers;
        each(splitStr(events), function(event) {
            handlers[event] = handlers[event] || [];
            handlers[event].push(handler);
        });
        return this;
    },

    /**
     * unbind event, leave emit blank to remove all handlers
     * @param {String} events
     * @param {Function} [handler]
     * @returns {EventEmitter} this
     */
    off: function(events, handler) {
        if (events === undefined) {
            return;
        }

        var handlers = this.handlers;
        each(splitStr(events), function(event) {
            if (!handler) {
                delete handlers[event];
            } else {
                handlers[event] && handlers[event].splice(inArray(handlers[event], handler), 1);
            }
        });
        return this;
    },

    /**
     * emit event to the listeners
     * @param {String} event
     * @param {Object} data
     */
    emit: function(event, data) {
        // we also want to trigger dom events
        if (this.options.domEvents) {
            triggerDomEvent(event, data);
        }

        // no handlers, so skip it all
        var handlers = this.handlers[event] && this.handlers[event].slice();
        if (!handlers || !handlers.length) {
            return;
        }

        data.type = event;
        data.preventDefault = function() {
            data.srcEvent.preventDefault();
        };

        var i = 0;
        while (i < handlers.length) {
            handlers[i](data);
            i++;
        }
    },

    /**
     * destroy the manager and unbinds all events
     * it doesn't unbind dom events, that is the user own responsibility
     */
    destroy: function() {
        this.element && toggleCssProps(this, false);

        this.handlers = {};
        this.session = {};
        this.input.destroy();
        this.element = null;
    }
};

/**
 * add/remove the css properties as defined in manager.options.cssProps
 * @param {Manager} manager
 * @param {Boolean} add
 */
function toggleCssProps(manager, add) {
    var element = manager.element;
    if (!element.style) {
        return;
    }
    var prop;
    each(manager.options.cssProps, function(value, name) {
        prop = prefixed(element.style, name);
        if (add) {
            manager.oldCssProps[prop] = element.style[prop];
            element.style[prop] = value;
        } else {
            element.style[prop] = manager.oldCssProps[prop] || '';
        }
    });
    if (!add) {
        manager.oldCssProps = {};
    }
}

/**
 * trigger dom event
 * @param {String} event
 * @param {Object} data
 */
function triggerDomEvent(event, data) {
    var gestureEvent = document.createEvent('Event');
    gestureEvent.initEvent(event, true, true);
    gestureEvent.gesture = data;
    data.target.dispatchEvent(gestureEvent);
}

assign(Hammer, {
    INPUT_START: INPUT_START,
    INPUT_MOVE: INPUT_MOVE,
    INPUT_END: INPUT_END,
    INPUT_CANCEL: INPUT_CANCEL,

    STATE_POSSIBLE: STATE_POSSIBLE,
    STATE_BEGAN: STATE_BEGAN,
    STATE_CHANGED: STATE_CHANGED,
    STATE_ENDED: STATE_ENDED,
    STATE_RECOGNIZED: STATE_RECOGNIZED,
    STATE_CANCELLED: STATE_CANCELLED,
    STATE_FAILED: STATE_FAILED,

    DIRECTION_NONE: DIRECTION_NONE,
    DIRECTION_LEFT: DIRECTION_LEFT,
    DIRECTION_RIGHT: DIRECTION_RIGHT,
    DIRECTION_UP: DIRECTION_UP,
    DIRECTION_DOWN: DIRECTION_DOWN,
    DIRECTION_HORIZONTAL: DIRECTION_HORIZONTAL,
    DIRECTION_VERTICAL: DIRECTION_VERTICAL,
    DIRECTION_ALL: DIRECTION_ALL,

    Manager: Manager,
    Input: Input,
    TouchAction: TouchAction,

    TouchInput: TouchInput,
    MouseInput: MouseInput,
    PointerEventInput: PointerEventInput,
    TouchMouseInput: TouchMouseInput,
    SingleTouchInput: SingleTouchInput,

    Recognizer: Recognizer,
    AttrRecognizer: AttrRecognizer,
    Tap: TapRecognizer,
    Pan: PanRecognizer,
    Swipe: SwipeRecognizer,
    Pinch: PinchRecognizer,
    Rotate: RotateRecognizer,
    Press: PressRecognizer,

    on: addEventListeners,
    off: removeEventListeners,
    each: each,
    merge: merge,
    extend: extend,
    assign: assign,
    inherit: inherit,
    bindFn: bindFn,
    prefixed: prefixed
});

// this prevents errors when Hammer is loaded in the presence of an AMD
//  style loader but by script tag, not by the loader.
var freeGlobal = (typeof window !== 'undefined' ? window : (typeof self !== 'undefined' ? self : {})); // jshint ignore:line
freeGlobal.Hammer = Hammer;

if (typeof define === 'function' && define.amd) {
    define(function() {
        return Hammer;
    });
} else if (typeof module != 'undefined' && module.exports) {
    module.exports = Hammer;
} else {
    window[exportName] = Hammer;
}

})(window, document, 'Hammer');

/*****
 * @licstart
 *
 * The following is the license notice for the part of JavaScript code of this
 * page included between the '@jessyinkstart' and the '@jessyinkend' notes.
 */

/*****  ******************************************************************
 *
 * Copyright 2008-2013 Hannes Hochreiner
 *
 * The JavaScript code included between the start note '@jessyinkstart'
 * and the end note '@jessyinkend' is subject to the terms of the Mozilla
 * Public License, v. 2.0. If a copy of the MPL was not distributed with
 * this file, You can obtain one at http://mozilla.org/MPL/2.0/.
 *
 * Alternatively, you can redistribute and/or that part of this file
 * 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/.
*/

/*****
 *  You can find the complete source code of the JessyInk project at:
 *  @source http://code.google.com/p/jessyink/
 */

/*****
 * @licend
 *
 * The above is the license notice for the part of JavaScript code of this
 * page included between the '@jessyinkstart' and the '@jessyinkend' notes.
 */



/*****
 * @jessyinkstart
 *
 *  The following code is a derivative work of some parts of the JessyInk
 *  project.
 *  @source http://code.google.com/p/jessyink/
 */

/** Convenience function to get an element depending on whether it has a
 *  property with a particular name.
 *
 *  @param node   element of the document
 *  @param name   attribute name
 *
 *  @returns   Array array containing all the elements of the tree with root
 *             'node' that own the property 'name'
 */
function getElementsByProperty( node, name )
{
    var elements = [];

    if( node.getAttribute( name ) )
        elements.push( node );

    for( var counter = 0; counter < node.childNodes.length; ++counter )
    {
        if( node.childNodes[counter].nodeType == 1 )
        {
            var subElements = getElementsByProperty( node.childNodes[counter], name );
            elements = elements.concat( subElements );
        }
    }
    return elements;
}

// User can hide / show the presentation content.
// For that purpose, we change the background color of root node to either black or white.
// To set it back to its original color (for showing the content back), we should have the initial background color saved somewhere to read back.
// There may be no initial color at all, so the initial value of the saved initial is undefined.
var rootNodeInitialBackgroundColor = undefined;

function changeRootNodeBackgroundTo(color) {
	if (rootNodeInitialBackgroundColor === undefined)
		rootNodeInitialBackgroundColor = ROOT_NODE.style.backgroundColor;

	if (color === 'initial')
		ROOT_NODE.style.backgroundColor = rootNodeInitialBackgroundColor;
	else
		ROOT_NODE.style.backgroundColor = color;
}

var isContentHidden = false;
var contentInitialVisibilityValues = null;

function getInitialVisibilityValues() {
	var list = ROOT_NODE.querySelectorAll('g');
	contentInitialVisibilityValues = [];
	for (var i = 0; i < list.length; i++) {
		var temp = {};
		temp.object = list[i];
		temp.visibility = list[i].style.visibility;
		contentInitialVisibilityValues.push(temp);
	}
}

function hideShowContent(color) {
	if (contentInitialVisibilityValues === null)
		getInitialVisibilityValues();

	if (isContentHidden) {
		for (var i = 0; i < contentInitialVisibilityValues.length; i++)
			contentInitialVisibilityValues[i].object.style.visibility = contentInitialVisibilityValues[i].visibility;

		changeRootNodeBackgroundTo('initial');
		isContentHidden = false;
	}
	else {
		for (var i = 0; i < contentInitialVisibilityValues.length; i++)
			contentInitialVisibilityValues[i].object.style.visibility = 'hidden';

		changeRootNodeBackgroundTo(color);
		isContentHidden = true;
	}
}

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

    var code = aEvt.keyCode || aEvt.charCode || aEvt.code;

    // console.log('===> onKeyDown: ' + code);

    // Handle arrow keys in iOS WebKit (including Mobile Safari)
    if (code == 0 && aEvt.key != undefined) {
        switch (aEvt.key) {
        case 'UIKeyInputLeftArrow':
            code = LEFT_KEY;
            break;
        case 'UIKeyInputUpArrow':
            code = UP_KEY;
            break;
        case 'UIKeyInputRightArrow':
            code = RIGHT_KEY;
            break;
        case 'UIKeyInputDownArrow':
            code = DOWN_KEY;
            break;
        }

        // console.log('     now: ' + code);
    }
	else if (code === P_KEY) {
		aEvt.preventDefault();
		if (ROOT_NODE.style.cursor === 'pointer')
			ROOT_NODE.style.cursor = 'default';
		else
			ROOT_NODE.style.cursor = 'pointer';
	}
	else if (code === W_KEY) {
		hideShowContent('white');
	}
	else if (code === B_KEY) {
		hideShowContent('black');
	}


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

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

    if ( !aEvt )
        aEvt = window.event;

    var str = String.fromCharCode( aEvt.keyCode || aEvt.charCode );

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

    return null;
}

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

    keyCodeDict[SLIDE_MODE] = {};
    keyCodeDict[INDEX_MODE] = {};

    // slide mode
    keyCodeDict[SLIDE_MODE][LEFT_KEY]
        = function() { return aSlideShow.rewindEffect(); };
    keyCodeDict[SLIDE_MODE][RIGHT_KEY]
        = function() { return dispatchEffects(1); };
    keyCodeDict[SLIDE_MODE][UP_KEY]
        = function() { return aSlideShow.rewindEffect(); };
    keyCodeDict[SLIDE_MODE][DOWN_KEY]
        = function() { return skipEffects(1); };
    keyCodeDict[SLIDE_MODE][PAGE_UP_KEY]
        = function() { return aSlideShow.rewindAllEffects(); };
    keyCodeDict[SLIDE_MODE][PAGE_DOWN_KEY]
        = function() { return skipAllEffects(); };
    keyCodeDict[SLIDE_MODE][HOME_KEY]
        = function() { return aSlideShow.displaySlide( 0, true ); };
    keyCodeDict[SLIDE_MODE][END_KEY]
        = function() { return aSlideShow.displaySlide( theMetaDoc.nNumberOfSlides - 1, true ); };
    keyCodeDict[SLIDE_MODE][SPACE_KEY]
        = function() { return dispatchEffects(1); };
    // The ESC key can't actually be handled on iOS, it seems to be hardcoded to work like the home button? But try anyway.
    keyCodeDict[SLIDE_MODE][ESCAPE_KEY]
        = function() { return aSlideShow.exitSlideShowInApp(); };
    keyCodeDict[SLIDE_MODE][Q_KEY]
        = function() { return aSlideShow.exitSlideShowInApp(); };

    // index mode
    keyCodeDict[INDEX_MODE][LEFT_KEY]
        = function() { return indexSetPageSlide( theSlideIndexPage.selectedSlideIndex - 1 ); };
    keyCodeDict[INDEX_MODE][RIGHT_KEY]
        = function() { return indexSetPageSlide( theSlideIndexPage.selectedSlideIndex + 1 ); };
    keyCodeDict[INDEX_MODE][UP_KEY]
        = function() { return indexSetPageSlide( theSlideIndexPage.selectedSlideIndex - theSlideIndexPage.indexColumns ); };
    keyCodeDict[INDEX_MODE][DOWN_KEY]
        = function() { return indexSetPageSlide( theSlideIndexPage.selectedSlideIndex + theSlideIndexPage.indexColumns ); };
    keyCodeDict[INDEX_MODE][PAGE_UP_KEY]
        = function() { return indexSetPageSlide( theSlideIndexPage.selectedSlideIndex - theSlideIndexPage.getTotalThumbnails() ); };
    keyCodeDict[INDEX_MODE][PAGE_DOWN_KEY]
        = function() { return indexSetPageSlide( theSlideIndexPage.selectedSlideIndex + theSlideIndexPage.getTotalThumbnails() ); };
    keyCodeDict[INDEX_MODE][HOME_KEY]
        = function() { return indexSetPageSlide( 0 ); };
    keyCodeDict[INDEX_MODE][END_KEY]
        = function() { return indexSetPageSlide( theMetaDoc.nNumberOfSlides - 1 ); };
    keyCodeDict[INDEX_MODE][ENTER_KEY]
        = function() { return toggleSlideIndex(); };
    keyCodeDict[INDEX_MODE][SPACE_KEY]
        = function() { return toggleSlideIndex(); };
    keyCodeDict[INDEX_MODE][ESCAPE_KEY]
        = function() { return abandonIndexMode(); };

    return keyCodeDict;
}

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

    charCodeDict[SLIDE_MODE] = {};
    charCodeDict[INDEX_MODE] = {};

    // slide mode
    charCodeDict[SLIDE_MODE]['i']
        = function () { return toggleSlideIndex(); };

    // index mode
    charCodeDict[INDEX_MODE]['i']
        = function () { return toggleSlideIndex(); };
    charCodeDict[INDEX_MODE]['-']
        = function () { return theSlideIndexPage.decreaseNumberOfColumns(); };
    charCodeDict[INDEX_MODE]['=']
        = function () { return theSlideIndexPage.increaseNumberOfColumns(); };
    charCodeDict[INDEX_MODE]['+']
        = function () { return theSlideIndexPage.increaseNumberOfColumns(); };
    charCodeDict[INDEX_MODE]['0']
        = function () { return theSlideIndexPage.resetNumberOfColumns(); };

    return charCodeDict;
}


function slideOnMouseUp( aEvt )
{
    if (!aEvt)
        aEvt = window.event;

    var nOffset = 0;

    if( aEvt.button == 0 )
        nOffset = 1;
    else if( aEvt.button == 2 )
        nOffset = -1;

    if( 0 != nOffset )
        dispatchEffects( nOffset );
    return true; // the click has been handled
}

document.handleClick = slideOnMouseUp;


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

    if (!aEvt)
        aEvt = window.event;

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

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

    if (aEvt.preventDefault)
        aEvt.preventDefault();

    aEvt.returnValue = false;
}

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

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

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

    var retVal = true;

    if ( mouseHandlerDictionary[currentMode] && mouseHandlerDictionary[currentMode][anAction] )
    {
        var subRetVal = mouseHandlerDictionary[currentMode][anAction]( aEvt );

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

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

    aEvt.returnValue = retVal;

    return retVal;
}

//Set mouse event handler.
document.onmouseup = function( aEvt ) { return mouseHandlerDispatch( aEvt, MOUSE_UP ); };


/** mouseClickHelper
 *
 * @return {Object}
 *   a mouse click handler
 */
function mouseClickHelper( aEvt )
{
    // In case text is selected we stay on the current slide.
    // Anyway if we are dealing with Firefox there is an issue:
    // Firefox supports a naive way of selecting svg text, if you click
    // on text the current selection is set to the whole text fragment
    // wrapped by the related <tspan> element.
    // That means until you click on text you never move to the next slide.
    // In order to avoid this case we do not test the status of current
    // selection, when the presentation is running on a mozilla browser.
    if( !Detect.isMozilla )
    {
        var aWindowObject = document.defaultView;
        if( aWindowObject )
        {
            var aTextSelection = aWindowObject.getSelection();
            var sSelectedText =  aTextSelection.toString();
            if( sSelectedText )
            {
                DBGLOG( 'text selection: ' + sSelectedText );
                if( sLastSelectedText !== sSelectedText )
                {
                    bTextHasBeenSelected = true;
                    sLastSelectedText = sSelectedText;
                }
                else
                {
                    bTextHasBeenSelected = false;
                }
                return null;
            }
            else if( bTextHasBeenSelected )
            {
                bTextHasBeenSelected = false;
                sLastSelectedText = '';
                return null;
            }
        }
        else
        {
            log( 'error: HyperlinkElement.handleClick: invalid window object.' );
        }
    }

    var aSlideAnimationsHandler = theMetaDoc.aMetaSlideSet[nCurSlide].aSlideAnimationsHandler;
    if( aSlideAnimationsHandler )
    {
        var aCurrentEventMultiplexer = aSlideAnimationsHandler.aEventMultiplexer;
        if( aCurrentEventMultiplexer )
        {
            if( aCurrentEventMultiplexer.hasRegisteredMouseClickHandlers() )
            {
                return aCurrentEventMultiplexer.notifyMouseClick( aEvt );
            }
        }
    }
    return slideOnMouseUp( aEvt );
}


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

    mouseHandlerDict[SLIDE_MODE] = {};
    mouseHandlerDict[INDEX_MODE] = {};

    // slide mode
    mouseHandlerDict[SLIDE_MODE][MOUSE_UP]
        = mouseClickHelper;

    mouseHandlerDict[SLIDE_MODE][MOUSE_WHEEL]
        = function( aEvt ) { return slideOnMouseWheel( aEvt ); };

    // index mode
    mouseHandlerDict[INDEX_MODE][MOUSE_UP]
        = function( ) { return toggleSlideIndex(); };

    return mouseHandlerDict;
}

/** Function to set the page and active slide in index view.
 *
 *  @param nIndex 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( nIndex )
{
    var aMetaSlideSet = theMetaDoc.aMetaSlideSet;
    nIndex = getSafeIndex( nIndex, 0, aMetaSlideSet.length - 1 );

    //calculate the offset
    var nSelectedThumbnailIndex = nIndex % theSlideIndexPage.getTotalThumbnails();
    var offset = nIndex - nSelectedThumbnailIndex;

    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 selected thumbnail and the current slide
    theSlideIndexPage.setSelection( nSelectedThumbnailIndex );
}


/*****
 * @jessyinkend
 *
 *  The above code is a derivative work of some parts of the JessyInk project.
 *  @source http://code.google.com/p/jessyink/
 */





/*****
 * @licstart
 *
 * The following is the license notice for the part of JavaScript code of this
 * page included between the '@dojostart' and the '@dojoend' notes.
 */

/*****  **********************************************************************
 *
 *  The 'New' BSD License:
 *  **********************
 *  Copyright (c) 2005-2012, The Dojo Foundation
 *  All rights reserved.
 *
 *  Redistribution and use in source and binary forms, with or without
 *  modification, are permitted provided that the following conditions are met:
 *
 *    * Redistributions of source code must retain the above copyright notice,
 *      this list of conditions and the following disclaimer.
 *    * Redistributions in binary form must reproduce the above copyright notice,
 *      this list of conditions and the following disclaimer in the documentation
 *      and/or other materials provided with the distribution.
 *    * Neither the name of the Dojo Foundation nor the names of its contributors
 *      may be used to endorse or promote products derived from this software
 *      without specific prior written permission.
 *
 *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 'AS IS' AND
 *  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 *  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 *  DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
 *  FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 *  DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 *  SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 *  CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 *  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 *  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 ****************************************************************************/


/*****
 * @licend
 *
 * The above is the license notice for the part of JavaScript code of this
 * page included between the '@dojostart' and the '@dojoend' notes.
 */



/*****
 * @dojostart
 *
 *  The following code is a derivative work of some part of the dojox.gfx library.
 *  @source http://svn.dojotoolkit.org/src/dojox/trunk/_base/sniff.js
 */

function has( name )
{
    return has.cache[name];
}

has.cache = {};

has.add = function( name, test )
{
    has.cache[name] = test;
};

function configureDetectionTools()
{
    if( !navigator )
    {
        log( 'error: configureDetectionTools: configuration failed' );
        return null;
    }

    var n = navigator,
    dua = n.userAgent,
    dav = n.appVersion,
    tv = parseFloat(dav);

    has.add('air', dua.indexOf('AdobeAIR') >= 0);
    has.add('khtml', dav.indexOf('Konqueror') >= 0 ? tv : undefined);
    has.add('webkit', parseFloat(dua.split('WebKit/')[1]) || undefined);
    has.add('chrome', parseFloat(dua.split('Chrome/')[1]) || undefined);
    has.add('safari', dav.indexOf('Safari')>=0 && !has('chrome') ? parseFloat(dav.split('Version/')[1]) : undefined);
    has.add('mac', dav.indexOf('Macintosh') >= 0);
    has.add('quirks', document.compatMode == 'BackCompat');
    has.add('ios', /iPhone|iPod|iPad/.test(dua));
    has.add('android', parseFloat(dua.split('Android ')[1]) || undefined);

    if(!has('webkit')){
        // Opera
        if(dua.indexOf('Opera') >= 0){
            // see http://dev.opera.com/articles/view/opera-ua-string-changes and http://www.useragentstring.com/pages/Opera/
            // 9.8 has both styles; <9.8, 9.9 only old style
            has.add('opera', tv >= 9.8 ? parseFloat(dua.split('Version/')[1]) || tv : tv);
        }

        // Mozilla and firefox
        if(dua.indexOf('Gecko') >= 0 && !has('khtml') && !has('webkit')){
            has.add('mozilla', tv);
        }
        if(has('mozilla')){
            //We really need to get away from this. Consider a sane isGecko approach for the future.
            has.add('ff', parseFloat(dua.split('Firefox/')[1] || dua.split('Minefield/')[1]) || undefined);
        }

        // IE
        if(document.all && !has('opera')){
            var isIE = parseFloat(dav.split('MSIE ')[1]) || undefined;

            //In cases where the page has an HTTP header or META tag with
            //X-UA-Compatible, then it is in emulation mode.
            //Make sure isIE reflects the desired version.
            //document.documentMode of 5 means quirks mode.
            //Only switch the value if documentMode's major version
            //is different from isIE major version.
            var mode = document.documentMode;
            if(mode && mode != 5 && Math.floor(isIE) != mode){
                isIE = mode;
            }

            has.add('ie', isIE);
        }

        // Wii
        has.add('wii', typeof opera != 'undefined' && opera.wiiremote);
    }

    var detect =
    {
		// isFF: Number|undefined
		//		Version as a Number if client is FireFox. undefined otherwise. Corresponds to
		//		major detected FireFox version (1.5, 2, 3, etc.)
		isFF: has('ff'),

		// isIE: Number|undefined
		//		Version as a Number if client is MSIE(PC). undefined otherwise. Corresponds to
		//		major detected IE version (6, 7, 8, etc.)
		isIE: has('ie'),

		// isKhtml: Number|undefined
		//		Version as a Number if client is a KHTML browser. undefined otherwise. Corresponds to major
		//		detected version.
		isKhtml: has('khtml'),

		// isWebKit: Number|undefined
		//		Version as a Number if client is a WebKit-derived browser (Konqueror,
		//		Safari, Chrome, etc.). undefined otherwise.
		isWebKit: has('webkit'),

		// isMozilla: Number|undefined
		//		Version as a Number if client is a Mozilla-based browser (Firefox,
		//		SeaMonkey). undefined otherwise. Corresponds to major detected version.
		isMozilla: has('mozilla'),
		// isMoz: Number|undefined
		//		Version as a Number if client is a Mozilla-based browser (Firefox,
		//		SeaMonkey). undefined otherwise. Corresponds to major detected version.
		isMoz: has('mozilla'),

		// isOpera: Number|undefined
		//		Version as a Number if client is Opera. undefined otherwise. Corresponds to
		//		major detected version.
		isOpera: has('opera'),

		// isSafari: Number|undefined
		//		Version as a Number if client is Safari or iPhone. undefined otherwise.
		isSafari: has('safari'),

		// isChrome: Number|undefined
		//		Version as a Number if client is Chrome browser. undefined otherwise.
		isChrome: has('chrome'),

		// isMac: Boolean
		//		True if the client runs on Mac
		isMac: has('mac'),

		// isIos: Boolean
		//		True if client is iPhone, iPod, or iPad
		isIos: has('ios'),

		// isAndroid: Number|undefined
		//		Version as a Number if client is android browser. undefined otherwise.
		isAndroid: has('android'),

		// isWii: Boolean
		//		True if client is Wii
		isWii: has('wii'),

		// isQuirks: Boolean
		//		Page is in quirks mode.
		isQuirks: has('quirks'),

		// isAir: Boolean
		//		True if client is Adobe Air
		isAir: has('air')
    };
    return detect;
}

/*****
 * @dojoend
 *
 *  The above code is a derivative work of some part of the dojox.gfx library.
 *  @source http://svn.dojotoolkit.org/src/dojox/trunk/_base/sniff.js
 */

/*****
 * @licstart
 *
 * The following is the license notice for the part of JavaScript code of this
 * file included between the '@svgpathstart' and the '@svgpathend' notes.
 */

/*****  **********************************************************************
 *
 *   Copyright 2015 The Chromium Authors. All rights reserved.
 *
 *   The Chromium Authors can be found at
 *   http://src.chromium.org/svn/trunk/src/AUTHORS
 *
 *   Redistribution and use in source and binary forms, with or without
 *   modification, are permitted provided that the following conditions are
 *   met:
 *
 *   * Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *   * Redistributions in binary form must reproduce the above
 *   copyright notice, this list of conditions and the following disclaimer
 *   in the documentation and/or other materials provided with the
 *   distribution.
 *   * Neither the name of Google Inc. nor the names of its
 *   contributors may be used to endorse or promote products derived from
 *   this software without specific prior written permission.
 *
 *   THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 *   'AS IS' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 *   LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 *   A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 *   OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 *   SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 *   LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 *   DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 *   THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 *   (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 *   OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 ****************************************************************************/

/*****
 * @licend
 *
 * The above is the license notice for the part of JavaScript code of this
 * file included between the '@svgpathstart' and the '@svgpathend' notes.
 */


/*****
 * @svgpathstart
 *
 *  The following code is a derivative work of some part of the SVGPathSeg API.
 *
 *  This API is a drop-in replacement for the SVGPathSeg and SVGPathSegList APIs that were removed from
 *  SVG2 (https://lists.w3.org/Archives/Public/www-svg/2015Jun/0044.html), including the latest spec
 *  changes which were implemented in Firefox 43 and Chrome 46.
 *
 *  @source https://github.com/progers/pathseg
 */

(function() { 'use strict';
    if (!('SVGPathSeg' in window)) {
        // Spec: http://www.w3.org/TR/SVG11/single-page.html#paths-InterfaceSVGPathSeg
        window.SVGPathSeg = function(type, typeAsLetter, owningPathSegList) {
            this.pathSegType = type;
            this.pathSegTypeAsLetter = typeAsLetter;
            this._owningPathSegList = owningPathSegList;
        }

        window.SVGPathSeg.prototype.classname = 'SVGPathSeg';

        window.SVGPathSeg.PATHSEG_UNKNOWN = 0;
        window.SVGPathSeg.PATHSEG_CLOSEPATH = 1;
        window.SVGPathSeg.PATHSEG_MOVETO_ABS = 2;
        window.SVGPathSeg.PATHSEG_MOVETO_REL = 3;
        window.SVGPathSeg.PATHSEG_LINETO_ABS = 4;
        window.SVGPathSeg.PATHSEG_LINETO_REL = 5;
        window.SVGPathSeg.PATHSEG_CURVETO_CUBIC_ABS = 6;
        window.SVGPathSeg.PATHSEG_CURVETO_CUBIC_REL = 7;
        window.SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_ABS = 8;
        window.SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_REL = 9;
        window.SVGPathSeg.PATHSEG_ARC_ABS = 10;
        window.SVGPathSeg.PATHSEG_ARC_REL = 11;
        window.SVGPathSeg.PATHSEG_LINETO_HORIZONTAL_ABS = 12;
        window.SVGPathSeg.PATHSEG_LINETO_HORIZONTAL_REL = 13;
        window.SVGPathSeg.PATHSEG_LINETO_VERTICAL_ABS = 14;
        window.SVGPathSeg.PATHSEG_LINETO_VERTICAL_REL = 15;
        window.SVGPathSeg.PATHSEG_CURVETO_CUBIC_SMOOTH_ABS = 16;
        window.SVGPathSeg.PATHSEG_CURVETO_CUBIC_SMOOTH_REL = 17;
        window.SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_SMOOTH_ABS = 18;
        window.SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_SMOOTH_REL = 19;

        // Notify owning PathSegList on any changes so they can be synchronized back to the path element.
        window.SVGPathSeg.prototype._segmentChanged = function() {
            if (this._owningPathSegList)
                this._owningPathSegList.segmentChanged(this);
        }

        window.SVGPathSegClosePath = function(owningPathSegList) {
            window.SVGPathSeg.call(this, window.SVGPathSeg.PATHSEG_CLOSEPATH, 'z', owningPathSegList);
        }
        window.SVGPathSegClosePath.prototype = Object.create(window.SVGPathSeg.prototype);
        window.SVGPathSegClosePath.prototype.toString = function() { return '[object SVGPathSegClosePath]'; }
        window.SVGPathSegClosePath.prototype._asPathString = function() { return this.pathSegTypeAsLetter; }
        window.SVGPathSegClosePath.prototype.clone = function() { return new window.SVGPathSegClosePath(undefined); }

        window.SVGPathSegMovetoAbs = function(owningPathSegList, x, y) {
            window.SVGPathSeg.call(this, window.SVGPathSeg.PATHSEG_MOVETO_ABS, 'M', owningPathSegList);
            this._x = x;
            this._y = y;
        }
        window.SVGPathSegMovetoAbs.prototype = Object.create(window.SVGPathSeg.prototype);
        window.SVGPathSegMovetoAbs.prototype.toString = function() { return '[object SVGPathSegMovetoAbs]'; }
        window.SVGPathSegMovetoAbs.prototype._asPathString = function() { return this.pathSegTypeAsLetter + ' ' + this._x + ' ' + this._y; }
        window.SVGPathSegMovetoAbs.prototype.clone = function() { return new window.SVGPathSegMovetoAbs(undefined, this._x, this._y); }
        Object.defineProperty(window.SVGPathSegMovetoAbs.prototype, 'x', { get: function() { return this._x; }, set: function(x) { this._x = x; this._segmentChanged(); }, enumerable: true });
        Object.defineProperty(window.SVGPathSegMovetoAbs.prototype, 'y', { get: function() { return this._y; }, set: function(y) { this._y = y; this._segmentChanged(); }, enumerable: true });

        window.SVGPathSegMovetoRel = function(owningPathSegList, x, y) {
            window.SVGPathSeg.call(this, window.SVGPathSeg.PATHSEG_MOVETO_REL, 'm', owningPathSegList);
            this._x = x;
            this._y = y;
        }
        window.SVGPathSegMovetoRel.prototype = Object.create(window.SVGPathSeg.prototype);
        window.SVGPathSegMovetoRel.prototype.toString = function() { return '[object SVGPathSegMovetoRel]'; }
        window.SVGPathSegMovetoRel.prototype._asPathString = function() { return this.pathSegTypeAsLetter + ' ' + this._x + ' ' + this._y; }
        window.SVGPathSegMovetoRel.prototype.clone = function() { return new window.SVGPathSegMovetoRel(undefined, this._x, this._y); }
        Object.defineProperty(window.SVGPathSegMovetoRel.prototype, 'x', { get: function() { return this._x; }, set: function(x) { this._x = x; this._segmentChanged(); }, enumerable: true });
        Object.defineProperty(window.SVGPathSegMovetoRel.prototype, 'y', { get: function() { return this._y; }, set: function(y) { this._y = y; this._segmentChanged(); }, enumerable: true });

        window.SVGPathSegLinetoAbs = function(owningPathSegList, x, y) {
            window.SVGPathSeg.call(this, window.SVGPathSeg.PATHSEG_LINETO_ABS, 'L', owningPathSegList);
            this._x = x;
            this._y = y;
        }
        window.SVGPathSegLinetoAbs.prototype = Object.create(window.SVGPathSeg.prototype);
        window.SVGPathSegLinetoAbs.prototype.toString = function() { return '[object SVGPathSegLinetoAbs]'; }
        window.SVGPathSegLinetoAbs.prototype._asPathString = function() { return this.pathSegTypeAsLetter + ' ' + this._x + ' ' + this._y; }
        window.SVGPathSegLinetoAbs.prototype.clone = function() { return new window.SVGPathSegLinetoAbs(undefined, this._x, this._y); }
        Object.defineProperty(window.SVGPathSegLinetoAbs.prototype, 'x', { get: function() { return this._x; }, set: function(x) { this._x = x; this._segmentChanged(); }, enumerable: true });
        Object.defineProperty(window.SVGPathSegLinetoAbs.prototype, 'y', { get: function() { return this._y; }, set: function(y) { this._y = y; this._segmentChanged(); }, enumerable: true });

        window.SVGPathSegLinetoRel = function(owningPathSegList, x, y) {
            window.SVGPathSeg.call(this, window.SVGPathSeg.PATHSEG_LINETO_REL, 'l', owningPathSegList);
            this._x = x;
            this._y = y;
        }
        window.SVGPathSegLinetoRel.prototype = Object.create(window.SVGPathSeg.prototype);
        window.SVGPathSegLinetoRel.prototype.toString = function() { return '[object SVGPathSegLinetoRel]'; }
        window.SVGPathSegLinetoRel.prototype._asPathString = function() { return this.pathSegTypeAsLetter + ' ' + this._x + ' ' + this._y; }
        window.SVGPathSegLinetoRel.prototype.clone = function() { return new window.SVGPathSegLinetoRel(undefined, this._x, this._y); }
        Object.defineProperty(window.SVGPathSegLinetoRel.prototype, 'x', { get: function() { return this._x; }, set: function(x) { this._x = x; this._segmentChanged(); }, enumerable: true });
        Object.defineProperty(window.SVGPathSegLinetoRel.prototype, 'y', { get: function() { return this._y; }, set: function(y) { this._y = y; this._segmentChanged(); }, enumerable: true });

        window.SVGPathSegCurvetoCubicAbs = function(owningPathSegList, x, y, x1, y1, x2, y2) {
            window.SVGPathSeg.call(this, window.SVGPathSeg.PATHSEG_CURVETO_CUBIC_ABS, 'C', owningPathSegList);
            this._x = x;
            this._y = y;
            this._x1 = x1;
            this._y1 = y1;
            this._x2 = x2;
            this._y2 = y2;
        }
        window.SVGPathSegCurvetoCubicAbs.prototype = Object.create(window.SVGPathSeg.prototype);
        window.SVGPathSegCurvetoCubicAbs.prototype.toString = function() { return '[object SVGPathSegCurvetoCubicAbs]'; }
        window.SVGPathSegCurvetoCubicAbs.prototype._asPathString = function() { return this.pathSegTypeAsLetter + ' ' + this._x1 + ' ' + this._y1 + ' ' + this._x2 + ' ' + this._y2 + ' ' + this._x + ' ' + this._y; }
        window.SVGPathSegCurvetoCubicAbs.prototype.clone = function() { return new window.SVGPathSegCurvetoCubicAbs(undefined, this._x, this._y, this._x1, this._y1, this._x2, this._y2); }
        Object.defineProperty(window.SVGPathSegCurvetoCubicAbs.prototype, 'x', { get: function() { return this._x; }, set: function(x) { this._x = x; this._segmentChanged(); }, enumerable: true });
        Object.defineProperty(window.SVGPathSegCurvetoCubicAbs.prototype, 'y', { get: function() { return this._y; }, set: function(y) { this._y = y; this._segmentChanged(); }, enumerable: true });
        Object.defineProperty(window.SVGPathSegCurvetoCubicAbs.prototype, 'x1', { get: function() { return this._x1; }, set: function(x1) { this._x1 = x1; this._segmentChanged(); }, enumerable: true });
        Object.defineProperty(window.SVGPathSegCurvetoCubicAbs.prototype, 'y1', { get: function() { return this._y1; }, set: function(y1) { this._y1 = y1; this._segmentChanged(); }, enumerable: true });
        Object.defineProperty(window.SVGPathSegCurvetoCubicAbs.prototype, 'x2', { get: function() { return this._x2; }, set: function(x2) { this._x2 = x2; this._segmentChanged(); }, enumerable: true });
        Object.defineProperty(window.SVGPathSegCurvetoCubicAbs.prototype, 'y2', { get: function() { return this._y2; }, set: function(y2) { this._y2 = y2; this._segmentChanged(); }, enumerable: true });

        window.SVGPathSegCurvetoCubicRel = function(owningPathSegList, x, y, x1, y1, x2, y2) {
            window.SVGPathSeg.call(this, window.SVGPathSeg.PATHSEG_CURVETO_CUBIC_REL, 'c', owningPathSegList);
            this._x = x;
            this._y = y;
            this._x1 = x1;
            this._y1 = y1;
            this._x2 = x2;
            this._y2 = y2;
        }
        window.SVGPathSegCurvetoCubicRel.prototype = Object.create(window.SVGPathSeg.prototype);
        window.SVGPathSegCurvetoCubicRel.prototype.toString = function() { return '[object SVGPathSegCurvetoCubicRel]'; }
        window.SVGPathSegCurvetoCubicRel.prototype._asPathString = function() { return this.pathSegTypeAsLetter + ' ' + this._x1 + ' ' + this._y1 + ' ' + this._x2 + ' ' + this._y2 + ' ' + this._x + ' ' + this._y; }
        window.SVGPathSegCurvetoCubicRel.prototype.clone = function() { return new window.SVGPathSegCurvetoCubicRel(undefined, this._x, this._y, this._x1, this._y1, this._x2, this._y2); }
        Object.defineProperty(window.SVGPathSegCurvetoCubicRel.prototype, 'x', { get: function() { return this._x; }, set: function(x) { this._x = x; this._segmentChanged(); }, enumerable: true });
        Object.defineProperty(window.SVGPathSegCurvetoCubicRel.prototype, 'y', { get: function() { return this._y; }, set: function(y) { this._y = y; this._segmentChanged(); }, enumerable: true });
        Object.defineProperty(window.SVGPathSegCurvetoCubicRel.prototype, 'x1', { get: function() { return this._x1; }, set: function(x1) { this._x1 = x1; this._segmentChanged(); }, enumerable: true });
        Object.defineProperty(window.SVGPathSegCurvetoCubicRel.prototype, 'y1', { get: function() { return this._y1; }, set: function(y1) { this._y1 = y1; this._segmentChanged(); }, enumerable: true });
        Object.defineProperty(window.SVGPathSegCurvetoCubicRel.prototype, 'x2', { get: function() { return this._x2; }, set: function(x2) { this._x2 = x2; this._segmentChanged(); }, enumerable: true });
        Object.defineProperty(window.SVGPathSegCurvetoCubicRel.prototype, 'y2', { get: function() { return this._y2; }, set: function(y2) { this._y2 = y2; this._segmentChanged(); }, enumerable: true });

        window.SVGPathSegCurvetoQuadraticAbs = function(owningPathSegList, x, y, x1, y1) {
            window.SVGPathSeg.call(this, window.SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_ABS, 'Q', owningPathSegList);
            this._x = x;
            this._y = y;
            this._x1 = x1;
            this._y1 = y1;
        }
        window.SVGPathSegCurvetoQuadraticAbs.prototype = Object.create(window.SVGPathSeg.prototype);
        window.SVGPathSegCurvetoQuadraticAbs.prototype.toString = function() { return '[object SVGPathSegCurvetoQuadraticAbs]'; }
        window.SVGPathSegCurvetoQuadraticAbs.prototype._asPathString = function() { return this.pathSegTypeAsLetter + ' ' + this._x1 + ' ' + this._y1 + ' ' + this._x + ' ' + this._y; }
        window.SVGPathSegCurvetoQuadraticAbs.prototype.clone = function() { return new window.SVGPathSegCurvetoQuadraticAbs(undefined, this._x, this._y, this._x1, this._y1); }
        Object.defineProperty(window.SVGPathSegCurvetoQuadraticAbs.prototype, 'x', { get: function() { return this._x; }, set: function(x) { this._x = x; this._segmentChanged(); }, enumerable: true });
        Object.defineProperty(window.SVGPathSegCurvetoQuadraticAbs.prototype, 'y', { get: function() { return this._y; }, set: function(y) { this._y = y; this._segmentChanged(); }, enumerable: true });
        Object.defineProperty(window.SVGPathSegCurvetoQuadraticAbs.prototype, 'x1', { get: function() { return this._x1; }, set: function(x1) { this._x1 = x1; this._segmentChanged(); }, enumerable: true });
        Object.defineProperty(window.SVGPathSegCurvetoQuadraticAbs.prototype, 'y1', { get: function() { return this._y1; }, set: function(y1) { this._y1 = y1; this._segmentChanged(); }, enumerable: true });

        window.SVGPathSegCurvetoQuadraticRel = function(owningPathSegList, x, y, x1, y1) {
            window.SVGPathSeg.call(this, window.SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_REL, 'q', owningPathSegList);
            this._x = x;
            this._y = y;
            this._x1 = x1;
            this._y1 = y1;
        }
        window.SVGPathSegCurvetoQuadraticRel.prototype = Object.create(window.SVGPathSeg.prototype);
        window.SVGPathSegCurvetoQuadraticRel.prototype.toString = function() { return '[object SVGPathSegCurvetoQuadraticRel]'; }
        window.SVGPathSegCurvetoQuadraticRel.prototype._asPathString = function() { return this.pathSegTypeAsLetter + ' ' + this._x1 + ' ' + this._y1 + ' ' + this._x + ' ' + this._y; }
        window.SVGPathSegCurvetoQuadraticRel.prototype.clone = function() { return new window.SVGPathSegCurvetoQuadraticRel(undefined, this._x, this._y, this._x1, this._y1); }
        Object.defineProperty(window.SVGPathSegCurvetoQuadraticRel.prototype, 'x', { get: function() { return this._x; }, set: function(x) { this._x = x; this._segmentChanged(); }, enumerable: true });
        Object.defineProperty(window.SVGPathSegCurvetoQuadraticRel.prototype, 'y', { get: function() { return this._y; }, set: function(y) { this._y = y; this._segmentChanged(); }, enumerable: true });
        Object.defineProperty(window.SVGPathSegCurvetoQuadraticRel.prototype, 'x1', { get: function() { return this._x1; }, set: function(x1) { this._x1 = x1; this._segmentChanged(); }, enumerable: true });
        Object.defineProperty(window.SVGPathSegCurvetoQuadraticRel.prototype, 'y1', { get: function() { return this._y1; }, set: function(y1) { this._y1 = y1; this._segmentChanged(); }, enumerable: true });

        window.SVGPathSegArcAbs = function(owningPathSegList, x, y, r1, r2, angle, largeArcFlag, sweepFlag) {
            window.SVGPathSeg.call(this, window.SVGPathSeg.PATHSEG_ARC_ABS, 'A', owningPathSegList);
            this._x = x;
            this._y = y;
            this._r1 = r1;
            this._r2 = r2;
            this._angle = angle;
            this._largeArcFlag = largeArcFlag;
            this._sweepFlag = sweepFlag;
        }
        window.SVGPathSegArcAbs.prototype = Object.create(window.SVGPathSeg.prototype);
        window.SVGPathSegArcAbs.prototype.toString = function() { return '[object SVGPathSegArcAbs]'; }
        window.SVGPathSegArcAbs.prototype._asPathString = function() { return this.pathSegTypeAsLetter + ' ' + this._r1 + ' ' + this._r2 + ' ' + this._angle + ' ' + (this._largeArcFlag ? '1' : '0') + ' ' + (this._sweepFlag ? '1' : '0') + ' ' + this._x + ' ' + this._y; }
        window.SVGPathSegArcAbs.prototype.clone = function() { return new window.SVGPathSegArcAbs(undefined, this._x, this._y, this._r1, this._r2, this._angle, this._largeArcFlag, this._sweepFlag); }
        Object.defineProperty(window.SVGPathSegArcAbs.prototype, 'x', { get: function() { return this._x; }, set: function(x) { this._x = x; this._segmentChanged(); }, enumerable: true });
        Object.defineProperty(window.SVGPathSegArcAbs.prototype, 'y', { get: function() { return this._y; }, set: function(y) { this._y = y; this._segmentChanged(); }, enumerable: true });
        Object.defineProperty(window.SVGPathSegArcAbs.prototype, 'r1', { get: function() { return this._r1; }, set: function(r1) { this._r1 = r1; this._segmentChanged(); }, enumerable: true });
        Object.defineProperty(window.SVGPathSegArcAbs.prototype, 'r2', { get: function() { return this._r2; }, set: function(r2) { this._r2 = r2; this._segmentChanged(); }, enumerable: true });
        Object.defineProperty(window.SVGPathSegArcAbs.prototype, 'angle', { get: function() { return this._angle; }, set: function(angle) { this._angle = angle; this._segmentChanged(); }, enumerable: true });
        Object.defineProperty(window.SVGPathSegArcAbs.prototype, 'largeArcFlag', { get: function() { return this._largeArcFlag; }, set: function(largeArcFlag) { this._largeArcFlag = largeArcFlag; this._segmentChanged(); }, enumerable: true });
        Object.defineProperty(window.SVGPathSegArcAbs.prototype, 'sweepFlag', { get: function() { return this._sweepFlag; }, set: function(sweepFlag) { this._sweepFlag = sweepFlag; this._segmentChanged(); }, enumerable: true });

        window.SVGPathSegArcRel = function(owningPathSegList, x, y, r1, r2, angle, largeArcFlag, sweepFlag) {
            window.SVGPathSeg.call(this, window.SVGPathSeg.PATHSEG_ARC_REL, 'a', owningPathSegList);
            this._x = x;
            this._y = y;
            this._r1 = r1;
            this._r2 = r2;
            this._angle = angle;
            this._largeArcFlag = largeArcFlag;
            this._sweepFlag = sweepFlag;
        }
        window.SVGPathSegArcRel.prototype = Object.create(window.SVGPathSeg.prototype);
        window.SVGPathSegArcRel.prototype.toString = function() { return '[object SVGPathSegArcRel]'; }
        window.SVGPathSegArcRel.prototype._asPathString = function() { return this.pathSegTypeAsLetter + ' ' + this._r1 + ' ' + this._r2 + ' ' + this._angle + ' ' + (this._largeArcFlag ? '1' : '0') + ' ' + (this._sweepFlag ? '1' : '0') + ' ' + this._x + ' ' + this._y; }
        window.SVGPathSegArcRel.prototype.clone = function() { return new window.SVGPathSegArcRel(undefined, this._x, this._y, this._r1, this._r2, this._angle, this._largeArcFlag, this._sweepFlag); }
        Object.defineProperty(window.SVGPathSegArcRel.prototype, 'x', { get: function() { return this._x; }, set: function(x) { this._x = x; this._segmentChanged(); }, enumerable: true });
        Object.defineProperty(window.SVGPathSegArcRel.prototype, 'y', { get: function() { return this._y; }, set: function(y) { this._y = y; this._segmentChanged(); }, enumerable: true });
        Object.defineProperty(window.SVGPathSegArcRel.prototype, 'r1', { get: function() { return this._r1; }, set: function(r1) { this._r1 = r1; this._segmentChanged(); }, enumerable: true });
        Object.defineProperty(window.SVGPathSegArcRel.prototype, 'r2', { get: function() { return this._r2; }, set: function(r2) { this._r2 = r2; this._segmentChanged(); }, enumerable: true });
        Object.defineProperty(window.SVGPathSegArcRel.prototype, 'angle', { get: function() { return this._angle; }, set: function(angle) { this._angle = angle; this._segmentChanged(); }, enumerable: true });
        Object.defineProperty(window.SVGPathSegArcRel.prototype, 'largeArcFlag', { get: function() { return this._largeArcFlag; }, set: function(largeArcFlag) { this._largeArcFlag = largeArcFlag; this._segmentChanged(); }, enumerable: true });
        Object.defineProperty(window.SVGPathSegArcRel.prototype, 'sweepFlag', { get: function() { return this._sweepFlag; }, set: function(sweepFlag) { this._sweepFlag = sweepFlag; this._segmentChanged(); }, enumerable: true });

        window.SVGPathSegLinetoHorizontalAbs = function(owningPathSegList, x) {
            window.SVGPathSeg.call(this, window.SVGPathSeg.PATHSEG_LINETO_HORIZONTAL_ABS, 'H', owningPathSegList);
            this._x = x;
        }
        window.SVGPathSegLinetoHorizontalAbs.prototype = Object.create(window.SVGPathSeg.prototype);
        window.SVGPathSegLinetoHorizontalAbs.prototype.toString = function() { return '[object SVGPathSegLinetoHorizontalAbs]'; }
        window.SVGPathSegLinetoHorizontalAbs.prototype._asPathString = function() { return this.pathSegTypeAsLetter + ' ' + this._x; }
        window.SVGPathSegLinetoHorizontalAbs.prototype.clone = function() { return new window.SVGPathSegLinetoHorizontalAbs(undefined, this._x); }
        Object.defineProperty(window.SVGPathSegLinetoHorizontalAbs.prototype, 'x', { get: function() { return this._x; }, set: function(x) { this._x = x; this._segmentChanged(); }, enumerable: true });

        window.SVGPathSegLinetoHorizontalRel = function(owningPathSegList, x) {
            window.SVGPathSeg.call(this, window.SVGPathSeg.PATHSEG_LINETO_HORIZONTAL_REL, 'h', owningPathSegList);
            this._x = x;
        }
        window.SVGPathSegLinetoHorizontalRel.prototype = Object.create(window.SVGPathSeg.prototype);
        window.SVGPathSegLinetoHorizontalRel.prototype.toString = function() { return '[object SVGPathSegLinetoHorizontalRel]'; }
        window.SVGPathSegLinetoHorizontalRel.prototype._asPathString = function() { return this.pathSegTypeAsLetter + ' ' + this._x; }
        window.SVGPathSegLinetoHorizontalRel.prototype.clone = function() { return new window.SVGPathSegLinetoHorizontalRel(undefined, this._x); }
        Object.defineProperty(window.SVGPathSegLinetoHorizontalRel.prototype, 'x', { get: function() { return this._x; }, set: function(x) { this._x = x; this._segmentChanged(); }, enumerable: true });

        window.SVGPathSegLinetoVerticalAbs = function(owningPathSegList, y) {
            window.SVGPathSeg.call(this, window.SVGPathSeg.PATHSEG_LINETO_VERTICAL_ABS, 'V', owningPathSegList);
            this._y = y;
        }
        window.SVGPathSegLinetoVerticalAbs.prototype = Object.create(window.SVGPathSeg.prototype);
        window.SVGPathSegLinetoVerticalAbs.prototype.toString = function() { return '[object SVGPathSegLinetoVerticalAbs]'; }
        window.SVGPathSegLinetoVerticalAbs.prototype._asPathString = function() { return this.pathSegTypeAsLetter + ' ' + this._y; }
        window.SVGPathSegLinetoVerticalAbs.prototype.clone = function() { return new window.SVGPathSegLinetoVerticalAbs(undefined, this._y); }
        Object.defineProperty(window.SVGPathSegLinetoVerticalAbs.prototype, 'y', { get: function() { return this._y; }, set: function(y) { this._y = y; this._segmentChanged(); }, enumerable: true });

        window.SVGPathSegLinetoVerticalRel = function(owningPathSegList, y) {
            window.SVGPathSeg.call(this, window.SVGPathSeg.PATHSEG_LINETO_VERTICAL_REL, 'v', owningPathSegList);
            this._y = y;
        }
        window.SVGPathSegLinetoVerticalRel.prototype = Object.create(window.SVGPathSeg.prototype);
        window.SVGPathSegLinetoVerticalRel.prototype.toString = function() { return '[object SVGPathSegLinetoVerticalRel]'; }
        window.SVGPathSegLinetoVerticalRel.prototype._asPathString = function() { return this.pathSegTypeAsLetter + ' ' + this._y; }
        window.SVGPathSegLinetoVerticalRel.prototype.clone = function() { return new window.SVGPathSegLinetoVerticalRel(undefined, this._y); }
        Object.defineProperty(window.SVGPathSegLinetoVerticalRel.prototype, 'y', { get: function() { return this._y; }, set: function(y) { this._y = y; this._segmentChanged(); }, enumerable: true });

        window.SVGPathSegCurvetoCubicSmoothAbs = function(owningPathSegList, x, y, x2, y2) {
            window.SVGPathSeg.call(this, window.SVGPathSeg.PATHSEG_CURVETO_CUBIC_SMOOTH_ABS, 'S', owningPathSegList);
            this._x = x;
            this._y = y;
            this._x2 = x2;
            this._y2 = y2;
        }
        window.SVGPathSegCurvetoCubicSmoothAbs.prototype = Object.create(window.SVGPathSeg.prototype);
        window.SVGPathSegCurvetoCubicSmoothAbs.prototype.toString = function() { return '[object SVGPathSegCurvetoCubicSmoothAbs]'; }
        window.SVGPathSegCurvetoCubicSmoothAbs.prototype._asPathString = function() { return this.pathSegTypeAsLetter + ' ' + this._x2 + ' ' + this._y2 + ' ' + this._x + ' ' + this._y; }
        window.SVGPathSegCurvetoCubicSmoothAbs.prototype.clone = function() { return new window.SVGPathSegCurvetoCubicSmoothAbs(undefined, this._x, this._y, this._x2, this._y2); }
        Object.defineProperty(window.SVGPathSegCurvetoCubicSmoothAbs.prototype, 'x', { get: function() { return this._x; }, set: function(x) { this._x = x; this._segmentChanged(); }, enumerable: true });
        Object.defineProperty(window.SVGPathSegCurvetoCubicSmoothAbs.prototype, 'y', { get: function() { return this._y; }, set: function(y) { this._y = y; this._segmentChanged(); }, enumerable: true });
        Object.defineProperty(window.SVGPathSegCurvetoCubicSmoothAbs.prototype, 'x2', { get: function() { return this._x2; }, set: function(x2) { this._x2 = x2; this._segmentChanged(); }, enumerable: true });
        Object.defineProperty(window.SVGPathSegCurvetoCubicSmoothAbs.prototype, 'y2', { get: function() { return this._y2; }, set: function(y2) { this._y2 = y2; this._segmentChanged(); }, enumerable: true });

        window.SVGPathSegCurvetoCubicSmoothRel = function(owningPathSegList, x, y, x2, y2) {
            window.SVGPathSeg.call(this, window.SVGPathSeg.PATHSEG_CURVETO_CUBIC_SMOOTH_REL, 's', owningPathSegList);
            this._x = x;
            this._y = y;
            this._x2 = x2;
            this._y2 = y2;
        }
        window.SVGPathSegCurvetoCubicSmoothRel.prototype = Object.create(window.SVGPathSeg.prototype);
        window.SVGPathSegCurvetoCubicSmoothRel.prototype.toString = function() { return '[object SVGPathSegCurvetoCubicSmoothRel]'; }
        window.SVGPathSegCurvetoCubicSmoothRel.prototype._asPathString = function() { return this.pathSegTypeAsLetter + ' ' + this._x2 + ' ' + this._y2 + ' ' + this._x + ' ' + this._y; }
        window.SVGPathSegCurvetoCubicSmoothRel.prototype.clone = function() { return new window.SVGPathSegCurvetoCubicSmoothRel(undefined, this._x, this._y, this._x2, this._y2); }
        Object.defineProperty(window.SVGPathSegCurvetoCubicSmoothRel.prototype, 'x', { get: function() { return this._x; }, set: function(x) { this._x = x; this._segmentChanged(); }, enumerable: true });
        Object.defineProperty(window.SVGPathSegCurvetoCubicSmoothRel.prototype, 'y', { get: function() { return this._y; }, set: function(y) { this._y = y; this._segmentChanged(); }, enumerable: true });
        Object.defineProperty(window.SVGPathSegCurvetoCubicSmoothRel.prototype, 'x2', { get: function() { return this._x2; }, set: function(x2) { this._x2 = x2; this._segmentChanged(); }, enumerable: true });
        Object.defineProperty(window.SVGPathSegCurvetoCubicSmoothRel.prototype, 'y2', { get: function() { return this._y2; }, set: function(y2) { this._y2 = y2; this._segmentChanged(); }, enumerable: true });

        window.SVGPathSegCurvetoQuadraticSmoothAbs = function(owningPathSegList, x, y) {
            window.SVGPathSeg.call(this, window.SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_SMOOTH_ABS, 'T', owningPathSegList);
            this._x = x;
            this._y = y;
        }
        window.SVGPathSegCurvetoQuadraticSmoothAbs.prototype = Object.create(window.SVGPathSeg.prototype);
        window.SVGPathSegCurvetoQuadraticSmoothAbs.prototype.toString = function() { return '[object SVGPathSegCurvetoQuadraticSmoothAbs]'; }
        window.SVGPathSegCurvetoQuadraticSmoothAbs.prototype._asPathString = function() { return this.pathSegTypeAsLetter + ' ' + this._x + ' ' + this._y; }
        window.SVGPathSegCurvetoQuadraticSmoothAbs.prototype.clone = function() { return new window.SVGPathSegCurvetoQuadraticSmoothAbs(undefined, this._x, this._y); }
        Object.defineProperty(window.SVGPathSegCurvetoQuadraticSmoothAbs.prototype, 'x', { get: function() { return this._x; }, set: function(x) { this._x = x; this._segmentChanged(); }, enumerable: true });
        Object.defineProperty(window.SVGPathSegCurvetoQuadraticSmoothAbs.prototype, 'y', { get: function() { return this._y; }, set: function(y) { this._y = y; this._segmentChanged(); }, enumerable: true });

        window.SVGPathSegCurvetoQuadraticSmoothRel = function(owningPathSegList, x, y) {
            window.SVGPathSeg.call(this, window.SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_SMOOTH_REL, 't', owningPathSegList);
            this._x = x;
            this._y = y;
        }
        window.SVGPathSegCurvetoQuadraticSmoothRel.prototype = Object.create(window.SVGPathSeg.prototype);
        window.SVGPathSegCurvetoQuadraticSmoothRel.prototype.toString = function() { return '[object SVGPathSegCurvetoQuadraticSmoothRel]'; }
        window.SVGPathSegCurvetoQuadraticSmoothRel.prototype._asPathString = function() { return this.pathSegTypeAsLetter + ' ' + this._x + ' ' + this._y; }
        window.SVGPathSegCurvetoQuadraticSmoothRel.prototype.clone = function() { return new window.SVGPathSegCurvetoQuadraticSmoothRel(undefined, this._x, this._y); }
        Object.defineProperty(window.SVGPathSegCurvetoQuadraticSmoothRel.prototype, 'x', { get: function() { return this._x; }, set: function(x) { this._x = x; this._segmentChanged(); }, enumerable: true });
        Object.defineProperty(window.SVGPathSegCurvetoQuadraticSmoothRel.prototype, 'y', { get: function() { return this._y; }, set: function(y) { this._y = y; this._segmentChanged(); }, enumerable: true });

        // Add createSVGPathSeg* functions to window.SVGPathElement.
        // Spec: http://www.w3.org/TR/SVG11/single-page.html#paths-Interfacewindow.SVGPathElement.
        window.SVGPathElement.prototype.createSVGPathSegClosePath = function() { return new window.SVGPathSegClosePath(undefined); }
        window.SVGPathElement.prototype.createSVGPathSegMovetoAbs = function(x, y) { return new window.SVGPathSegMovetoAbs(undefined, x, y); }
        window.SVGPathElement.prototype.createSVGPathSegMovetoRel = function(x, y) { return new window.SVGPathSegMovetoRel(undefined, x, y); }
        window.SVGPathElement.prototype.createSVGPathSegLinetoAbs = function(x, y) { return new window.SVGPathSegLinetoAbs(undefined, x, y); }
        window.SVGPathElement.prototype.createSVGPathSegLinetoRel = function(x, y) { return new window.SVGPathSegLinetoRel(undefined, x, y); }
        window.SVGPathElement.prototype.createSVGPathSegCurvetoCubicAbs = function(x, y, x1, y1, x2, y2) { return new window.SVGPathSegCurvetoCubicAbs(undefined, x, y, x1, y1, x2, y2); }
        window.SVGPathElement.prototype.createSVGPathSegCurvetoCubicRel = function(x, y, x1, y1, x2, y2) { return new window.SVGPathSegCurvetoCubicRel(undefined, x, y, x1, y1, x2, y2); }
        window.SVGPathElement.prototype.createSVGPathSegCurvetoQuadraticAbs = function(x, y, x1, y1) { return new window.SVGPathSegCurvetoQuadraticAbs(undefined, x, y, x1, y1); }
        window.SVGPathElement.prototype.createSVGPathSegCurvetoQuadraticRel = function(x, y, x1, y1) { return new window.SVGPathSegCurvetoQuadraticRel(undefined, x, y, x1, y1); }
        window.SVGPathElement.prototype.createSVGPathSegArcAbs = function(x, y, r1, r2, angle, largeArcFlag, sweepFlag) { return new window.SVGPathSegArcAbs(undefined, x, y, r1, r2, angle, largeArcFlag, sweepFlag); }
        window.SVGPathElement.prototype.createSVGPathSegArcRel = function(x, y, r1, r2, angle, largeArcFlag, sweepFlag) { return new window.SVGPathSegArcRel(undefined, x, y, r1, r2, angle, largeArcFlag, sweepFlag); }
        window.SVGPathElement.prototype.createSVGPathSegLinetoHorizontalAbs = function(x) { return new window.SVGPathSegLinetoHorizontalAbs(undefined, x); }
        window.SVGPathElement.prototype.createSVGPathSegLinetoHorizontalRel = function(x) { return new window.SVGPathSegLinetoHorizontalRel(undefined, x); }
        window.SVGPathElement.prototype.createSVGPathSegLinetoVerticalAbs = function(y) { return new window.SVGPathSegLinetoVerticalAbs(undefined, y); }
        window.SVGPathElement.prototype.createSVGPathSegLinetoVerticalRel = function(y) { return new window.SVGPathSegLinetoVerticalRel(undefined, y); }
        window.SVGPathElement.prototype.createSVGPathSegCurvetoCubicSmoothAbs = function(x, y, x2, y2) { return new window.SVGPathSegCurvetoCubicSmoothAbs(undefined, x, y, x2, y2); }
        window.SVGPathElement.prototype.createSVGPathSegCurvetoCubicSmoothRel = function(x, y, x2, y2) { return new window.SVGPathSegCurvetoCubicSmoothRel(undefined, x, y, x2, y2); }
        window.SVGPathElement.prototype.createSVGPathSegCurvetoQuadraticSmoothAbs = function(x, y) { return new window.SVGPathSegCurvetoQuadraticSmoothAbs(undefined, x, y); }
        window.SVGPathElement.prototype.createSVGPathSegCurvetoQuadraticSmoothRel = function(x, y) { return new window.SVGPathSegCurvetoQuadraticSmoothRel(undefined, x, y); }

        if (!('getPathSegAtLength' in window.SVGPathElement.prototype)) {
            // Add getPathSegAtLength to SVGPathElement.
            // Spec: https://www.w3.org/TR/SVG11/single-page.html#paths-__svg__SVGPathElement__getPathSegAtLength
            // This polyfill requires SVGPathElement.getTotalLength to implement the distance-along-a-path algorithm.
            window.SVGPathElement.prototype.getPathSegAtLength = function(distance) {
                if (distance === undefined || !isFinite(distance))
                    throw 'Invalid arguments.';

                var measurementElement = document.createElementNS('http://www.w3.org/2000/svg', 'path');
                measurementElement.setAttribute('d', this.getAttribute('d'));
                var lastPathSegment = measurementElement.pathSegList.numberOfItems - 1;

                // If the path is empty, return 0.
                if (lastPathSegment <= 0)
                    return 0;

                do {
                    measurementElement.pathSegList.removeItem(lastPathSegment);
                    if (distance > measurementElement.getTotalLength())
                        break;
                    lastPathSegment--;
                } while (lastPathSegment > 0);
                return lastPathSegment;
            }
        }
    }

    // Checking for SVGPathSegList in window checks for the case of an implementation without the
    // SVGPathSegList API.
    // The second check for appendItem is specific to Firefox 59+ which removed only parts of the
    // SVGPathSegList API (e.g., appendItem). In this case we need to re-implement the entire API
    // so the polyfill data (i.e., _list) is used throughout.
    if (!('SVGPathSegList' in window) || !('appendItem' in window.SVGPathSegList.prototype)) {
        // Spec: http://www.w3.org/TR/SVG11/single-page.html#paths-InterfaceSVGPathSegList
        window.SVGPathSegList = function(pathElement) {
            this._pathElement = pathElement;
            this._list = this._parsePath(this._pathElement.getAttribute('d'));

            // Use a MutationObserver to catch changes to the path's 'd' attribute.
            this._mutationObserverConfig = { 'attributes': true, 'attributeFilter': ['d'] };
            this._pathElementMutationObserver = new MutationObserver(this._updateListFromPathMutations.bind(this));
            this._pathElementMutationObserver.observe(this._pathElement, this._mutationObserverConfig);
        }

        window.SVGPathSegList.prototype.classname = 'SVGPathSegList';

        Object.defineProperty(window.SVGPathSegList.prototype, 'numberOfItems', {
            get: function() {
                this._checkPathSynchronizedToList();
                return this._list.length;
            },
            enumerable: true
        });

        // The length property was not specified but was in Firefox 58.
        Object.defineProperty(window.SVGPathSegList.prototype, 'length', {
            get: function() {
                this._checkPathSynchronizedToList();
                return this._list.length;
            },
            enumerable: true
        });

        // Add the pathSegList accessors to window.SVGPathElement.
        // Spec: http://www.w3.org/TR/SVG11/single-page.html#paths-InterfaceSVGAnimatedPathData
        Object.defineProperty(window.SVGPathElement.prototype, 'pathSegList', {
            get: function() {
                if (!this._pathSegList)
                    this._pathSegList = new window.SVGPathSegList(this);
                return this._pathSegList;
            },
            enumerable: true
        });
        // FIXME: The following are not implemented and simply return window.SVGPathElement.pathSegList.
        Object.defineProperty(window.SVGPathElement.prototype, 'normalizedPathSegList', { get: function() { return this.pathSegList; }, enumerable: true });
        Object.defineProperty(window.SVGPathElement.prototype, 'animatedPathSegList', { get: function() { return this.pathSegList; }, enumerable: true });
        Object.defineProperty(window.SVGPathElement.prototype, 'animatedNormalizedPathSegList', { get: function() { return this.pathSegList; }, enumerable: true });

        // Process any pending mutations to the path element and update the list as needed.
        // This should be the first call of all public functions and is needed because
        // MutationObservers are not synchronous so we can have pending asynchronous mutations.
        window.SVGPathSegList.prototype._checkPathSynchronizedToList = function() {
            this._updateListFromPathMutations(this._pathElementMutationObserver.takeRecords());
        }

        window.SVGPathSegList.prototype._updateListFromPathMutations = function(mutationRecords) {
            if (!this._pathElement)
                return;
            var hasPathMutations = false;
            mutationRecords.forEach(function(record) {
                if (record.attributeName == 'd')
                    hasPathMutations = true;
            });
            if (hasPathMutations)
                this._list = this._parsePath(this._pathElement.getAttribute('d'));
        }

        // Serialize the list and update the path's 'd' attribute.
        window.SVGPathSegList.prototype._writeListToPath = function() {
            this._pathElementMutationObserver.disconnect();
            this._pathElement.setAttribute('d', window.SVGPathSegList._pathSegArrayAsString(this._list));
            this._pathElementMutationObserver.observe(this._pathElement, this._mutationObserverConfig);
        }

        // When a path segment changes the list needs to be synchronized back to the path element.
        window.SVGPathSegList.prototype.segmentChanged = function(pathSeg) {
            this._writeListToPath();
        }

        window.SVGPathSegList.prototype.clear = function() {
            this._checkPathSynchronizedToList();

            this._list.forEach(function(pathSeg) {
                pathSeg._owningPathSegList = null;
            });
            this._list = [];
            this._writeListToPath();
        }

        window.SVGPathSegList.prototype.initialize = function(newItem) {
            this._checkPathSynchronizedToList();

            this._list = [newItem];
            newItem._owningPathSegList = this;
            this._writeListToPath();
            return newItem;
        }

        window.SVGPathSegList.prototype._checkValidIndex = function(index) {
            if (isNaN(index) || index < 0 || index >= this.numberOfItems)
                throw 'INDEX_SIZE_ERR';
        }

        window.SVGPathSegList.prototype.getItem = function(index) {
            this._checkPathSynchronizedToList();

            this._checkValidIndex(index);
            return this._list[index];
        }

        window.SVGPathSegList.prototype.insertItemBefore = function(newItem, index) {
            this._checkPathSynchronizedToList();

            // Spec: If the index is greater than or equal to numberOfItems, then the new item is appended to the end of the list.
            if (index > this.numberOfItems)
                index = this.numberOfItems;
            if (newItem._owningPathSegList) {
                // SVG2 spec says to make a copy.
                newItem = newItem.clone();
            }
            this._list.splice(index, 0, newItem);
            newItem._owningPathSegList = this;
            this._writeListToPath();
            return newItem;
        }

        window.SVGPathSegList.prototype.replaceItem = function(newItem, index) {
            this._checkPathSynchronizedToList();

            if (newItem._owningPathSegList) {
                // SVG2 spec says to make a copy.
                newItem = newItem.clone();
            }
            this._checkValidIndex(index);
            this._list[index] = newItem;
            newItem._owningPathSegList = this;
            this._writeListToPath();
            return newItem;
        }

        window.SVGPathSegList.prototype.removeItem = function(index) {
            this._checkPathSynchronizedToList();

            this._checkValidIndex(index);
            var item = this._list[index];
            this._list.splice(index, 1);
            this._writeListToPath();
            return item;
        }

        window.SVGPathSegList.prototype.appendItem = function(newItem) {
            this._checkPathSynchronizedToList();

            if (newItem._owningPathSegList) {
                // SVG2 spec says to make a copy.
                newItem = newItem.clone();
            }
            this._list.push(newItem);
            newItem._owningPathSegList = this;
            // TODO: Optimize this to just append to the existing attribute.
            this._writeListToPath();
            return newItem;
        };

        window.SVGPathSegList.prototype.matrixTransform = function(aSVGMatrix) {
            this._checkPathSynchronizedToList();

            var nLength = this._list.length;
            for( var i = 0; i < nLength; ++i )
            {
                var nX;
                var aPathSeg = this._list[i];
                switch( aPathSeg.pathSegTypeAsLetter )
                {
                    case 'C':
                        nX = aPathSeg._x2;
                        aPathSeg._x2 = aSVGMatrix.a * nX + aSVGMatrix.c * aPathSeg._y2 + aSVGMatrix.e;
                        aPathSeg._y2 = aSVGMatrix.b * nX + aSVGMatrix.d * aPathSeg._y2 + aSVGMatrix.f;
                    // fall through intended
                    case 'Q':
                        nX = aPathSeg._x1;
                        aPathSeg._x1 = aSVGMatrix.a * nX + aSVGMatrix.c * aPathSeg._y1 + aSVGMatrix.e;
                        aPathSeg._y1 = aSVGMatrix.b * nX + aSVGMatrix.d * aPathSeg._y1 + aSVGMatrix.f;
                    // fall through intended
                    case 'M':
                    case 'L':
                        nX = aPathSeg._x;
                        aPathSeg._x = aSVGMatrix.a * nX + aSVGMatrix.c * aPathSeg._y + aSVGMatrix.e;
                        aPathSeg._y = aSVGMatrix.b * nX + aSVGMatrix.d * aPathSeg._y + aSVGMatrix.f;
                        break;
                    default:
                        log( 'SVGPathSeg.matrixTransform: unexpected path segment type: '
                            + aPathSeg.pathSegTypeAsLetter );
                }
            }

            this._writeListToPath();
        };

        window.SVGPathSegList.prototype.changeOrientation = function() {
            this._checkPathSynchronizedToList();

            var aPathSegList = this._list;
            var nLength = aPathSegList.length;
            if( nLength == 0 ) return;

            var nCurrentX = 0;
            var nCurrentY = 0;

            var aPathSeg = aPathSegList[0];
            if( aPathSeg.pathSegTypeAsLetter == 'M' )
            {
                nCurrentX = aPathSeg.x;
                nCurrentY = aPathSeg.y;
                aPathSegList.shift();
                --nLength;
            }

            var i;
            for( i = 0; i < nLength; ++i )
            {
                aPathSeg = aPathSegList[i];
                switch( aPathSeg.pathSegTypeAsLetter )
                {
                    case 'C':
                        var nX = aPathSeg._x1;
                        aPathSeg._x1 = aPathSeg._x2;
                        aPathSeg._x2 = nX;
                        var nY = aPathSeg._y1;
                        aPathSeg._y1 = aPathSeg._y2;
                        aPathSeg._y2 = nY;
                    // fall through intended
                    case 'M':
                    case 'L':
                    case 'Q':
                        var aPoint = { x: aPathSeg._x, y: aPathSeg._y };
                        aPathSeg._x = nCurrentX;
                        aPathSeg._y = nCurrentY;
                        nCurrentX = aPoint.x;
                        nCurrentY = aPoint.y;
                        break;
                    default:
                        log( 'SVGPathSegList.changeOrientation: unexpected path segment type: '
                            + aPathSeg.pathSegTypeAsLetter );
                }

        }

            aPathSegList.reverse();

            var aMovePathSeg = new window.SVGPathSegMovetoAbs( this, nCurrentX, nCurrentY );
            aPathSegList.unshift( aMovePathSeg );

            this._writeListToPath();
        };

        window.SVGPathSegList._pathSegArrayAsString = function(pathSegArray) {
            var string = '';
            var first = true;
            pathSegArray.forEach(function(pathSeg) {
                if (first) {
                    first = false;
                    string += pathSeg._asPathString();
                } else {
                    string += ' ' + pathSeg._asPathString();
                }
            });
            return string;
        }

        // This closely follows SVGPathParser::parsePath from Source/core/svg/SVGPathParser.cpp.
        window.SVGPathSegList.prototype._parsePath = function(string) {
            if (!string || string.length == 0)
                return [];

            var owningPathSegList = this;

            var Builder = function() {
                this.pathSegList = [];
            }

            Builder.prototype.appendSegment = function(pathSeg) {
                this.pathSegList.push(pathSeg);
            }

            var Source = function(string) {
                this._string = string;
                this._currentIndex = 0;
                this._endIndex = this._string.length;
                this._previousCommand = window.SVGPathSeg.PATHSEG_UNKNOWN;

                this._skipOptionalSpaces();
            }

            Source.prototype._isCurrentSpace = function() {
                var character = this._string[this._currentIndex];
                return character <= ' ' && (character == ' ' || character == '\n' || character == '\t' || character == '\r' || character == '\f');
            }

            Source.prototype._skipOptionalSpaces = function() {
                while (this._currentIndex < this._endIndex && this._isCurrentSpace())
                    this._currentIndex++;
                return this._currentIndex < this._endIndex;
            }

            Source.prototype._skipOptionalSpacesOrDelimiter = function() {
                if (this._currentIndex < this._endIndex && !this._isCurrentSpace() && this._string.charAt(this._currentIndex) != ',')
                    return false;
                if (this._skipOptionalSpaces()) {
                    if (this._currentIndex < this._endIndex && this._string.charAt(this._currentIndex) == ',') {
                        this._currentIndex++;
                        this._skipOptionalSpaces();
                    }
                }
                return this._currentIndex < this._endIndex;
            }

            Source.prototype.hasMoreData = function() {
                return this._currentIndex < this._endIndex;
            }

            Source.prototype.peekSegmentType = function() {
                var lookahead = this._string[this._currentIndex];
                return this._pathSegTypeFromChar(lookahead);
            }

            Source.prototype._pathSegTypeFromChar = function(lookahead) {
                switch (lookahead) {
                    case 'Z':
                    case 'z':
                        return window.SVGPathSeg.PATHSEG_CLOSEPATH;
                    case 'M':
                        return window.SVGPathSeg.PATHSEG_MOVETO_ABS;
                    case 'm':
                        return window.SVGPathSeg.PATHSEG_MOVETO_REL;
                    case 'L':
                        return window.SVGPathSeg.PATHSEG_LINETO_ABS;
                    case 'l':
                        return window.SVGPathSeg.PATHSEG_LINETO_REL;
                    case 'C':
                        return window.SVGPathSeg.PATHSEG_CURVETO_CUBIC_ABS;
                    case 'c':
                        return window.SVGPathSeg.PATHSEG_CURVETO_CUBIC_REL;
                    case 'Q':
                        return window.SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_ABS;
                    case 'q':
                        return window.SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_REL;
                    case 'A':
                        return window.SVGPathSeg.PATHSEG_ARC_ABS;
                    case 'a':
                        return window.SVGPathSeg.PATHSEG_ARC_REL;
                    case 'H':
                        return window.SVGPathSeg.PATHSEG_LINETO_HORIZONTAL_ABS;
                    case 'h':
                        return window.SVGPathSeg.PATHSEG_LINETO_HORIZONTAL_REL;
                    case 'V':
                        return window.SVGPathSeg.PATHSEG_LINETO_VERTICAL_ABS;
                    case 'v':
                        return window.SVGPathSeg.PATHSEG_LINETO_VERTICAL_REL;
                    case 'S':
                        return window.SVGPathSeg.PATHSEG_CURVETO_CUBIC_SMOOTH_ABS;
                    case 's':
                        return window.SVGPathSeg.PATHSEG_CURVETO_CUBIC_SMOOTH_REL;
                    case 'T':
                        return window.SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_SMOOTH_ABS;
                    case 't':
                        return window.SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_SMOOTH_REL;
                    default:
                        return window.SVGPathSeg.PATHSEG_UNKNOWN;
                }
            }

            Source.prototype._nextCommandHelper = function(lookahead, previousCommand) {
                // Check for remaining coordinates in the current command.
                if ((lookahead == '+' || lookahead == '-' || lookahead == '.' || (lookahead >= '0' && lookahead <= '9')) && previousCommand != window.SVGPathSeg.PATHSEG_CLOSEPATH) {
                    if (previousCommand == window.SVGPathSeg.PATHSEG_MOVETO_ABS)
                        return window.SVGPathSeg.PATHSEG_LINETO_ABS;
                    if (previousCommand == window.SVGPathSeg.PATHSEG_MOVETO_REL)
                        return window.SVGPathSeg.PATHSEG_LINETO_REL;
                    return previousCommand;
                }
                return window.SVGPathSeg.PATHSEG_UNKNOWN;
            }

            Source.prototype.initialCommandIsMoveTo = function() {
                // If the path is empty it is still valid, so return true.
                if (!this.hasMoreData())
                    return true;
                var command = this.peekSegmentType();
                // Path must start with moveTo.
                return command == window.SVGPathSeg.PATHSEG_MOVETO_ABS || command == window.SVGPathSeg.PATHSEG_MOVETO_REL;
            }

            // Parse a number from an SVG path. This very closely follows genericParseNumber(...) from Source/core/svg/SVGParserUtilities.cpp.
            // Spec: http://www.w3.org/TR/SVG11/single-page.html#paths-PathDataBNF
            Source.prototype._parseNumber = function() {
                var exponent = 0;
                var integer = 0;
                var frac = 1;
                var decimal = 0;
                var sign = 1;
                var expsign = 1;

                var startIndex = this._currentIndex;

                this._skipOptionalSpaces();

                // Read the sign.
                if (this._currentIndex < this._endIndex && this._string.charAt(this._currentIndex) == '+')
                    this._currentIndex++;
                else if (this._currentIndex < this._endIndex && this._string.charAt(this._currentIndex) == '-') {
                    this._currentIndex++;
                    sign = -1;
                }

                if (this._currentIndex == this._endIndex || ((this._string.charAt(this._currentIndex) < '0' || this._string.charAt(this._currentIndex) > '9') && this._string.charAt(this._currentIndex) != '.'))
                // The first character of a number must be one of [0-9+-.].
                    return undefined;

                // Read the integer part, build right-to-left.
                var startIntPartIndex = this._currentIndex;
                while (this._currentIndex < this._endIndex && this._string.charAt(this._currentIndex) >= '0' && this._string.charAt(this._currentIndex) <= '9')
                    this._currentIndex++; // Advance to first non-digit.

                if (this._currentIndex != startIntPartIndex) {
                    var scanIntPartIndex = this._currentIndex - 1;
                    var multiplier = 1;
                    while (scanIntPartIndex >= startIntPartIndex) {
                        integer += multiplier * (this._string.charAt(scanIntPartIndex--) - '0');
                        multiplier *= 10;
                    }
                }

                // Read the decimals.
                if (this._currentIndex < this._endIndex && this._string.charAt(this._currentIndex) == '.') {
                    this._currentIndex++;

                    // There must be a least one digit following the .
                    if (this._currentIndex >= this._endIndex || this._string.charAt(this._currentIndex) < '0' || this._string.charAt(this._currentIndex) > '9')
                        return undefined;
                    while (this._currentIndex < this._endIndex && this._string.charAt(this._currentIndex) >= '0' && this._string.charAt(this._currentIndex) <= '9') {
                        frac *= 10;
                        decimal += (this._string.charAt(this._currentIndex) - '0') / frac;
                        this._currentIndex += 1;
                    }
                }

                // Read the exponent part.
                if (this._currentIndex != startIndex && this._currentIndex + 1 < this._endIndex && (this._string.charAt(this._currentIndex) == 'e' || this._string.charAt(this._currentIndex) == 'E') && (this._string.charAt(this._currentIndex + 1) != 'x' && this._string.charAt(this._currentIndex + 1) != 'm')) {
                    this._currentIndex++;

                    // Read the sign of the exponent.
                    if (this._string.charAt(this._currentIndex) == '+') {
                        this._currentIndex++;
                    } else if (this._string.charAt(this._currentIndex) == '-') {
                        this._currentIndex++;
                        expsign = -1;
                    }

                    // There must be an exponent.
                    if (this._currentIndex >= this._endIndex || this._string.charAt(this._currentIndex) < '0' || this._string.charAt(this._currentIndex) > '9')
                        return undefined;

                    while (this._currentIndex < this._endIndex && this._string.charAt(this._currentIndex) >= '0' && this._string.charAt(this._currentIndex) <= '9') {
                        exponent *= 10;
                        exponent += (this._string.charAt(this._currentIndex) - '0');
                        this._currentIndex++;
                    }
                }

                var number = integer + decimal;
                number *= sign;

                if (exponent)
                    number *= Math.pow(10, expsign * exponent);

                if (startIndex == this._currentIndex)
                    return undefined;

                this._skipOptionalSpacesOrDelimiter();

                return number;
            }

            Source.prototype._parseArcFlag = function() {
                if (this._currentIndex >= this._endIndex)
                    return undefined;
                var flag = false;
                var flagChar = this._string.charAt(this._currentIndex++);
                if (flagChar == '0')
                    flag = false;
                else if (flagChar == '1')
                    flag = true;
                else
                    return undefined;

                this._skipOptionalSpacesOrDelimiter();
                return flag;
            }

            Source.prototype.parseSegment = function() {
                var lookahead = this._string[this._currentIndex];
                var command = this._pathSegTypeFromChar(lookahead);
                if (command == window.SVGPathSeg.PATHSEG_UNKNOWN) {
                    // Possibly an implicit command. Not allowed if this is the first command.
                    if (this._previousCommand == window.SVGPathSeg.PATHSEG_UNKNOWN)
                        return null;
                    command = this._nextCommandHelper(lookahead, this._previousCommand);
                    if (command == window.SVGPathSeg.PATHSEG_UNKNOWN)
                        return null;
                } else {
                    this._currentIndex++;
                }

                this._previousCommand = command;

                switch (command) {
                    case window.SVGPathSeg.PATHSEG_MOVETO_REL:
                        return new window.SVGPathSegMovetoRel(owningPathSegList, this._parseNumber(), this._parseNumber());
                    case window.SVGPathSeg.PATHSEG_MOVETO_ABS:
                        return new window.SVGPathSegMovetoAbs(owningPathSegList, this._parseNumber(), this._parseNumber());
                    case window.SVGPathSeg.PATHSEG_LINETO_REL:
                        return new window.SVGPathSegLinetoRel(owningPathSegList, this._parseNumber(), this._parseNumber());
                    case window.SVGPathSeg.PATHSEG_LINETO_ABS:
                        return new window.SVGPathSegLinetoAbs(owningPathSegList, this._parseNumber(), this._parseNumber());
                    case window.SVGPathSeg.PATHSEG_LINETO_HORIZONTAL_REL:
                        return new window.SVGPathSegLinetoHorizontalRel(owningPathSegList, this._parseNumber());
                    case window.SVGPathSeg.PATHSEG_LINETO_HORIZONTAL_ABS:
                        return new window.SVGPathSegLinetoHorizontalAbs(owningPathSegList, this._parseNumber());
                    case window.SVGPathSeg.PATHSEG_LINETO_VERTICAL_REL:
                        return new window.SVGPathSegLinetoVerticalRel(owningPathSegList, this._parseNumber());
                    case window.SVGPathSeg.PATHSEG_LINETO_VERTICAL_ABS:
                        return new window.SVGPathSegLinetoVerticalAbs(owningPathSegList, this._parseNumber());
                    case window.SVGPathSeg.PATHSEG_CLOSEPATH:
                        this._skipOptionalSpaces();
                        return new window.SVGPathSegClosePath(owningPathSegList);
                    case window.SVGPathSeg.PATHSEG_CURVETO_CUBIC_REL:
                        var points = {x1: this._parseNumber(), y1: this._parseNumber(), x2: this._parseNumber(), y2: this._parseNumber(), x: this._parseNumber(), y: this._parseNumber()};
                        return new window.SVGPathSegCurvetoCubicRel(owningPathSegList, points.x, points.y, points.x1, points.y1, points.x2, points.y2);
                    case window.SVGPathSeg.PATHSEG_CURVETO_CUBIC_ABS:
                        var points = {x1: this._parseNumber(), y1: this._parseNumber(), x2: this._parseNumber(), y2: this._parseNumber(), x: this._parseNumber(), y: this._parseNumber()};
                        return new window.SVGPathSegCurvetoCubicAbs(owningPathSegList, points.x, points.y, points.x1, points.y1, points.x2, points.y2);
                    case window.SVGPathSeg.PATHSEG_CURVETO_CUBIC_SMOOTH_REL:
                        var points = {x2: this._parseNumber(), y2: this._parseNumber(), x: this._parseNumber(), y: this._parseNumber()};
                        return new window.SVGPathSegCurvetoCubicSmoothRel(owningPathSegList, points.x, points.y, points.x2, points.y2);
                    case window.SVGPathSeg.PATHSEG_CURVETO_CUBIC_SMOOTH_ABS:
                        var points = {x2: this._parseNumber(), y2: this._parseNumber(), x: this._parseNumber(), y: this._parseNumber()};
                        return new window.SVGPathSegCurvetoCubicSmoothAbs(owningPathSegList, points.x, points.y, points.x2, points.y2);
                    case window.SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_REL:
                        var points = {x1: this._parseNumber(), y1: this._parseNumber(), x: this._parseNumber(), y: this._parseNumber()};
                        return new window.SVGPathSegCurvetoQuadraticRel(owningPathSegList, points.x, points.y, points.x1, points.y1);
                    case window.SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_ABS:
                        var points = {x1: this._parseNumber(), y1: this._parseNumber(), x: this._parseNumber(), y: this._parseNumber()};
                        return new window.SVGPathSegCurvetoQuadraticAbs(owningPathSegList, points.x, points.y, points.x1, points.y1);
                    case window.SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_SMOOTH_REL:
                        return new window.SVGPathSegCurvetoQuadraticSmoothRel(owningPathSegList, this._parseNumber(), this._parseNumber());
                    case window.SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_SMOOTH_ABS:
                        return new window.SVGPathSegCurvetoQuadraticSmoothAbs(owningPathSegList, this._parseNumber(), this._parseNumber());
                    case window.SVGPathSeg.PATHSEG_ARC_REL:
                        var points = {x1: this._parseNumber(), y1: this._parseNumber(), arcAngle: this._parseNumber(), arcLarge: this._parseArcFlag(), arcSweep: this._parseArcFlag(), x: this._parseNumber(), y: this._parseNumber()};
                        return new window.SVGPathSegArcRel(owningPathSegList, points.x, points.y, points.x1, points.y1, points.arcAngle, points.arcLarge, points.arcSweep);
                    case window.SVGPathSeg.PATHSEG_ARC_ABS:
                        var points = {x1: this._parseNumber(), y1: this._parseNumber(), arcAngle: this._parseNumber(), arcLarge: this._parseArcFlag(), arcSweep: this._parseArcFlag(), x: this._parseNumber(), y: this._parseNumber()};
                        return new window.SVGPathSegArcAbs(owningPathSegList, points.x, points.y, points.x1, points.y1, points.arcAngle, points.arcLarge, points.arcSweep);
                    default:
                        throw 'Unknown path seg type.'
                }
            }

            var builder = new Builder();
            var source = new Source(string);

            if (!source.initialCommandIsMoveTo())
                return [];
            while (source.hasMoreData()) {
                var pathSeg = source.parseSegment();
                if (!pathSeg)
                    return [];
                builder.appendSegment(pathSeg);
            }

            return builder.pathSegList;
        }
    }
}());

/*****
 * @svgpathend
 *
 *  The above code is a derivative work of some part of the SVGPathSeg API.
 *
 *  This API is a drop-in replacement for the SVGPathSeg and SVGPathSegList APIs that were removed from
 *  SVG2 (https://lists.w3.org/Archives/Public/www-svg/2015Jun/0044.html), including the latest spec
 *  changes which were implemented in Firefox 43 and Chrome 46.
 *
 *  @source https://github.com/progers/pathseg
 */


/*****
 * @licstart
 *
 * The following is the license notice for the part of JavaScript code  of
 * this page included between the '@libreofficestart' and the '@libreofficeend'
 * notes.
 */

/*****  ******************************************************************
 *
 * This file is part of the LibreOffice project.
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 *
 * This file incorporates work covered by the following license notice:
 *
 *   Licensed to the Apache Software Foundation (ASF) under one or more
 *   contributor license agreements. See the NOTICE file distributed
 *   with this work for additional information regarding copyright
 *   ownership. The ASF licenses this file to you under the Apache
 *   License, Version 2.0 (the 'License'); you may not use this file
 *   except in compliance with the License. You may obtain a copy of
 *   the License at http://www.apache.org/licenses/LICENSE-2.0 .
 *
 ************************************************************************/

/*****
 * @licend
 *
 * The above is the license notice for the part of JavaScript code  of
 * this page included between the '@libreofficestart' and the '@libreofficeend'
 * notes.
 */


/*****
 * @libreofficestart
 *
 * Several parts of the following code are the result of the porting,
 * started on August 2011, of the C++ code included in the source
 * files placed under the folder '/slideshow/source' and
 * sub-folders. This got later rebased onto the AL2-licensed versions
 * of those files in early 2013.
 * @source https://cgit.freedesktop.org/libreoffice/core/tree/slideshow/source
 *
 */


window.onload = init;


// ooo elements
var aOOOElemMetaSlides = 'ooo:meta_slides';
var aOOOElemMetaSlide = 'ooo:meta_slide';
var aOOOElemTextField = 'ooo:text_field';
var aPresentationClipPathId = 'presentation_clip_path';
var aPresentationClipPathShrinkId = 'presentation_clip_path_shrink';

// ooo attributes
var aOOOAttrNumberOfSlides = 'number-of-slides';
var aOOOAttrStartSlideNumber= 'start-slide-number';
var aOOOAttrNumberingType = 'page-numbering-type';
var aOOOAttrListItemNumberingType= 'numbering-type';
var aOOOAttrUsePositionedChars = 'use-positioned-chars';

var aOOOAttrSlide = 'slide';
var aOOOAttrMaster = 'master';
var aOOOAttrDisplayName = 'display-name';
var aOOOAttrSlideDuration = 'slide-duration';
var aOOOAttrHasTransition = 'has-transition';
var aOOOAttrHasCustomBackground = 'has-custom-background';
var aOOOAttrBackgroundVisibility = 'background-visibility';
var aOOOAttrMasterObjectsVisibility = 'master-objects-visibility';
var aOOOAttrPageNumberVisibility = 'page-number-visibility';
var aOOOAttrDateTimeVisibility = 'date-time-visibility';
var aOOOAttrFooterVisibility = 'footer-visibility';
var aOOOAttrHeaderVisibility = 'header-visibility';
var aOOOAttrDateTimeField = 'date-time-field';
var aOOOAttrFooterField = 'footer-field';
var aOOOAttrHeaderField = 'header-field';

var aOOOAttrDateTimeFormat = 'date-time-format';

var aOOOAttrTextAdjust = 'text-adjust';

// element class names
var aClipPathGroupClassName = 'ClipPathGroup';
var aPageClassName = 'Page';
var aSlideNumberClassName = 'PageNumber';
var aDateTimeClassName = 'DateTime';
var aFooterClassName = 'Footer';
var aHeaderClassName = 'Header';
var aDateClassName = 'Date';
var aTimeClassName = 'Time';
var aSlideNameClassName='PageName';

// Creating a namespace dictionary.
var NSS = {};
NSS['svg']='http://www.w3.org/2000/svg';
NSS['rdf']='http://www.w3.org/1999/02/22-rdf-syntax-ns#';
NSS['xlink']='http://www.w3.org/1999/xlink';
NSS['xml']='http://www.w3.org/XML/1998/namespace';
NSS['ooo'] = 'http://xml.openoffice.org/svg/export';
NSS['presentation'] = 'http://sun.com/xmlns/staroffice/presentation';
NSS['smil'] = 'http://www.w3.org/2001/SMIL20/';
NSS['anim'] = 'urn:oasis:names:tc:opendocument:xmlns:animation:1.0';

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

// Mouse handler actions.
var MOUSE_UP = 1;
var MOUSE_DOWN = 2; // eslint-disable-line no-unused-vars
var MOUSE_MOVE = 3; // eslint-disable-line no-unused-vars
var MOUSE_WHEEL = 4;

// Key-codes.
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;
var SPACE_KEY = 32;
var ESCAPE_KEY = 27;
var B_KEY = 66;
var P_KEY = 80;
var Q_KEY = 81;
var W_KEY = 87;

// Visibility Values
var HIDDEN = 0;
var VISIBLE = 1;
var INHERIT = 2;
var aVisibilityAttributeValue = [ 'hidden', 'visible', 'inherit' ];  // eslint-disable-line no-unused-vars
var aVisibilityValue = { 'hidden' : HIDDEN, 'visible' : VISIBLE, 'inherit' : INHERIT };

// Parameters
var ROOT_NODE = document.getElementsByTagNameNS( NSS['svg'], 'svg' )[0];
var WIDTH = 0;
var HEIGHT = 0;
var INDEX_COLUMNS_DEFAULT = 3;
var INDEX_OFFSET = 0;

// Initialization.
var Detect = configureDetectionTools();
var theMetaDoc;
var theSlideIndexPage;
var currentMode = SLIDE_MODE;
var processingEffect = false;
var nCurSlide = undefined;
var bTextHasBeenSelected = false;
var sLastSelectedText = '';


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

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

/***************************
 ** OOP support functions **
 ***************************/

function object( aObject )
{
    var F = function() {};
    F.prototype = aObject;
    return new F();
}


function extend( aSubType, aSuperType )
{
    if (!aSuperType || !aSubType)
    {
        alert('extend failed, verify dependencies');
    }
    var OP = Object.prototype;
    var sp = aSuperType.prototype;
    var rp = object( sp );
    aSubType.prototype = rp;

    rp.constructor = aSubType;
    aSubType.superclass = sp;

    // assign constructor property
    if (aSuperType != Object && sp.constructor == OP.constructor)
    {
        sp.constructor = aSuperType;
    }

    return aSubType;
}


function instantiate( TemplateClass, BaseType )
{
    if( !TemplateClass.instanceSet )
        TemplateClass.instanceSet = [];

    var nSize = TemplateClass.instanceSet.length;

    for( var i = 0; i < nSize; ++i )
    {
        if( TemplateClass.instanceSet[i].base === BaseType )
            return TemplateClass.instanceSet[i].instance;
    }

    TemplateClass.instanceSet[ nSize ] = {};
    TemplateClass.instanceSet[ nSize ].base = BaseType;
    TemplateClass.instanceSet[ nSize ].instance = TemplateClass( BaseType );

    return TemplateClass.instanceSet[ nSize ].instance;
}



/**********************************
 ** Helper functions and classes **
 **********************************/

function Rectangle( aSVGRectElem )
{
    var x = parseInt( aSVGRectElem.getAttribute( 'x' ) );
    var y = parseInt( aSVGRectElem.getAttribute( 'y' ) );
    var width = parseInt( aSVGRectElem.getAttribute( 'width' ) );
    var height = parseInt( aSVGRectElem.getAttribute( 'height' ) );

    this.left = x;
    this.right = x + width;
    this.top = y;
    this.bottom = y + height;
}

/*
 * Returns key corresponding to a value in object, null otherwise.
 *
 * @param Object
 * @param value
 */
function getKeyByValue(aObj, value) {
  for(var key in aObj) {
    if(aObj[key] == value)
      return key;
  }
  return null;
}

function log( message )
{
    if( typeof console == 'object' )
    {
        // eslint-disable-next-line no-console
        console.log( message );
    }
    else if( typeof opera == 'object' )
    {
        opera.postError( message );
    }
    // eslint-disable-next-line no-undef
    else if( typeof java == 'object' && typeof java.lang == 'object' )
    {
        // eslint-disable-next-line no-undef
        java.lang.System.out.println( message );
    }
}

function getNSAttribute( sNSPrefix, aElem, sAttrName )
{
    if( !aElem ) return null;
    if( 'getAttributeNS' in aElem )
    {
        return aElem.getAttributeNS( NSS[sNSPrefix], sAttrName );
    }
    else
    {
        return aElem.getAttribute( sNSPrefix + ':' + sAttrName );
    }
}

function getOOOAttribute( aElem, sAttrName )
{
    return getNSAttribute( 'ooo', aElem, sAttrName );
}

function setNSAttribute( sNSPrefix, aElem, sAttrName, aValue )
{
    if( !aElem ) return false;
    if( 'setAttributeNS' in aElem )
    {
        aElem.setAttributeNS( NSS[sNSPrefix], sAttrName, aValue );
        return true;
    }
    else
    {
        aElem.setAttribute(sNSPrefix + ':' + sAttrName, aValue );
        return true;
    }
}

function getElementsByClassName( aElem, sClassName )
{

    var aElementSet = [];
    // not all browsers support the 'getElementsByClassName' method
    if( 'getElementsByClassName' in aElem )
    {
        aElementSet = aElem.getElementsByClassName( sClassName );
    }
    else
    {
        var aElementSetByClassProperty = getElementsByProperty( aElem, 'class' );
        for( var i = 0; i < aElementSetByClassProperty.length; ++i )
        {
            var sAttrClassName = aElementSetByClassProperty[i].getAttribute( 'class' );
            if( sAttrClassName == sClassName )
            {
                aElementSet.push( aElementSetByClassProperty[i] );
            }
        }
    }
    return aElementSet;
}

function getElementByClassName( aElem, sClassName /*, sTagName */)
{
    var aElementSet = getElementsByClassName( aElem, sClassName );
    if ( aElementSet.length == 1 )
        return aElementSet[0];
    else
        return null;
}

function getClassAttribute(  aElem )
{
    if( aElem )
        return aElem.getAttribute( 'class' );
    return '';
}

function createElementGroup( aParentElement, aElementList, nFrom, nCount, sGroupClass, sGroupId )
{
    var nTo = nFrom + nCount;
    if( nCount < 1 || aElementList.length < nTo )
    {
        log( 'createElementGroup: not enough elements available.' );
        return;
    }
    var firstElement = aElementList[nFrom];
    if( !firstElement )
    {
        log( 'createElementGroup: element not found.' );
        return;
    }
    var aGroupElement = document.createElementNS( NSS['svg'], 'g' );
    if( sGroupId )
        aGroupElement.setAttribute( 'id', sGroupId );
    if( sGroupClass )
        aGroupElement.setAttribute( 'class', sGroupClass );
    aParentElement.insertBefore( aGroupElement, firstElement );
    var i = nFrom;
    for( ; i < nTo; ++i )
    {
        aParentElement.removeChild( aElementList[i] );
        aGroupElement.appendChild( aElementList[i] );
    }
}

function initVisibilityProperty( aElement )
{
    var nVisibility = VISIBLE;
    var sVisibility = aElement.getAttribute( 'visibility' );
    if( sVisibility ) nVisibility = aVisibilityValue[ sVisibility ];
    return nVisibility;
}

function getSafeIndex( nIndex, nMin, nMax )
{
    if( nIndex < nMin )
        return nMin;
    else if( nIndex > nMax )
        return nMax;
    else
        return nIndex;
}

function getUrlParameter(name)
{
    name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]');
    var regex = new RegExp('[\\?&]' + name + '=([^&#]*)');
    var results = regex.exec(window.location.search);
    return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' '));
}

/** getRandomInt
 *
 * @param nMax
 * @returns {number}
 *   an integer in [0,nMax[
 */
function getRandomInt( nMax )
{
    return Math.floor( Math.random() * nMax );
}


/*********************
 ** Debug Utilities **
 *********************/

function DebugPrinter()
{
    this.bEnabled = false;
}


DebugPrinter.prototype.on = function()
{
    this.bEnabled = true;
};

DebugPrinter.prototype.off = function()
{
    this.bEnabled = false;
};

DebugPrinter.prototype.isEnabled = function()
{
    return this.bEnabled;
};

DebugPrinter.prototype.print = function( sMessage, nTime )
{
    if( this.isEnabled() )
    {
        var sInfo = 'DBG: ' + sMessage;
        if( nTime )
            sInfo += ' (at: ' + String( nTime / 1000 ) + 's)';
        log( sInfo );
    }
};


// - Debug Printers -
var aGenericDebugPrinter = new DebugPrinter();
aGenericDebugPrinter.on();
var DBGLOG = bind2( DebugPrinter.prototype.print, aGenericDebugPrinter );

var NAVDBG = new DebugPrinter();
NAVDBG.off();

var ANIMDBG = new DebugPrinter();
ANIMDBG.off();

var aRegisterEventDebugPrinter = new DebugPrinter();
aRegisterEventDebugPrinter.off();

var aTimerEventQueueDebugPrinter = new DebugPrinter();
aTimerEventQueueDebugPrinter.off();

var aEventMultiplexerDebugPrinter = new DebugPrinter();
aEventMultiplexerDebugPrinter.off();

var aNextEffectEventArrayDebugPrinter = new DebugPrinter();
aNextEffectEventArrayDebugPrinter.off();

var aActivityQueueDebugPrinter = new DebugPrinter();
aActivityQueueDebugPrinter.off();

var aAnimatedElementDebugPrinter = new DebugPrinter();
aAnimatedElementDebugPrinter.off();




/************************
 ***   Core Classes   ***
 ************************/

/** Class MetaDocument
 *  This class provides a pool of properties related to the whole presentation.
 *  Moreover it is responsible for:
 *  - initializing the set of MetaSlide objects that handle the meta information
 *    for each slide;
 *  - creating a map with key an id and value the svg element containing
 *    the animations performed on the slide with such an id.
 *
 */
function MetaDocument()
{
    // We look for the svg element that provides the following presentation
    // properties:
    // - the number of slides in the presentation;
    // - the type of numbering used in the presentation.
    // Moreover it wraps svg elements providing meta information on each slide
    // and svg elements providing content and properties of each text field.
    var aMetaDocElem = document.getElementById( aOOOElemMetaSlides );
    assert( aMetaDocElem, 'MetaDocument: the svg element with id:' + aOOOElemMetaSlides + 'is not valid.');

    // We initialize general presentation properties:
    // - the number of slides in the presentation;
    this.nNumberOfSlides = parseInt( aMetaDocElem.getAttributeNS( NSS['ooo'], aOOOAttrNumberOfSlides ) );
    assert( typeof this.nNumberOfSlides == 'number' && this.nNumberOfSlides > 0,
            'MetaDocument: number of slides is zero or undefined.' );
    // - the index of the slide to show when the presentation starts;
    this.nStartSlideNumber = parseInt( aMetaDocElem.getAttributeNS( NSS['ooo'], aOOOAttrStartSlideNumber ) ) || 0;
    // - get the parameter StartSlideNumber in the URL for online presentation
    var aParmStartSlideNumber = getUrlParameter('StartSlideNumber');
    if (aParmStartSlideNumber !== '')
    {
        this.nStartSlideNumber = parseInt(aParmStartSlideNumber);
    }
    // - the numbering type used in the presentation, default type is arabic.
    this.sPageNumberingType = aMetaDocElem.getAttributeNS( NSS['ooo'], aOOOAttrNumberingType ) || 'arabic';
    // - the way text is exported
    this.bIsUsePositionedChars = ( aMetaDocElem.getAttributeNS( NSS['ooo'], aOOOAttrUsePositionedChars ) === 'true' );

    // The <defs> element used for wrapping <clipPath>.
    this.aClipPathGroup = getElementByClassName( ROOT_NODE, aClipPathGroupClassName );
    assert( this.aClipPathGroup, 'MetaDocument: the clip path group element is not valid.');

    // The <clipPath> element used to clip all slides.
    this.aPresentationClipPath = document.getElementById( aPresentationClipPathId );
    assert( this.aPresentationClipPath,
            'MetaDocument: the presentation clip path element element is not valid.');

    // The collections for handling properties of each slide, svg elements
    // related to master pages and content and properties of text fields.
    this.aMetaSlideSet = [];
    this.aMasterPageSet = {};
    this.aTextFieldHandlerSet = {};
    this.aTextFieldContentProviderSet = [];
    this.aSlideNumberProvider = new SlideNumberProvider( this.nStartSlideNumber + 1, this.sPageNumberingType );
    this.aCurrentDateProvider = new CurrentDateTimeProvider( null, '<date>' );
    this.aCurrentTimeProvider = new CurrentDateTimeProvider( null, '<time>' );

    // We create a map with key an id and value the svg element containing
    // the animations performed on the slide with such an id.
    this.bIsAnimated = false;
    this.aSlideAnimationsMap = {};
    this.initSlideAnimationsMap();

    // We initialize dummy slide - used as leaving slide for transition on the first slide
    this.theMetaDummySlide = new MetaSlide( 'ooo:meta_dummy_slide', this );

    // We initialize the set of MetaSlide objects that handle the meta
    // information for each slide.
    for( var i = 0; i < this.nNumberOfSlides; ++i )
    {
        var sMetaSlideId = aOOOElemMetaSlide + '_' + i;
        this.aMetaSlideSet.push( new MetaSlide( sMetaSlideId, this ) );
    }
    assert( this.aMetaSlideSet.length == this.nNumberOfSlides,
            'MetaDocument: aMetaSlideSet.length != nNumberOfSlides.' );
}

MetaDocument.prototype =
{
/*** public methods ***/

/** getCurrentSlide
 *
 *  @return
 *      The MetaSlide object handling the current slide.
 */
getCurrentSlide : function()
{
    return this.aMetaSlideSet[nCurSlide];
},

/** setCurrentSlide
 *
 *  @param nSlideIndex
 *      The index of the slide to show.
 */
setCurrentSlide : function( nSlideIndex )
{
    if( nSlideIndex >= 0 &&  nSlideIndex < this.nNumberOfSlides )
    {
        if( nCurSlide !== undefined )
            this.aMetaSlideSet[nCurSlide].hide();
        this.aMetaSlideSet[nSlideIndex].show();
        nCurSlide = nSlideIndex;
    }
    else
    {
        log('MetaDocument.setCurrentSlide: slide index out of range: ' + nSlideIndex );
    }
},

/*** private methods ***/

initSlideAnimationsMap : function()
{
    var aAnimationsSection = document.getElementById( 'presentation-animations' );
    if( aAnimationsSection )
    {
        var aAnimationsDefSet = aAnimationsSection.getElementsByTagName( 'defs' );

        // we have at least one slide with animations ?
        this.bIsAnimated = ( typeof aAnimationsDefSet.length =='number' &&
                             aAnimationsDefSet.length > 0 );

        for( var i = 0; i < aAnimationsDefSet.length; ++i )
        {
            var sSlideId = aAnimationsDefSet[i].getAttributeNS( NSS['ooo'], aOOOAttrSlide );
            var aChildSet = getElementChildren( aAnimationsDefSet[i] );
            if( sSlideId && ( aChildSet.length === 1 ) )
            {
                this.aSlideAnimationsMap[ sSlideId ] = aChildSet[0];
            }
        }
    }
}

}; // end MetaDocument prototype

/** Class MetaSlide
 *  This class is responsible for:
 *  - parsing and initializing slide properties;
 *  - creating a MasterSlide object that provides direct access to the target
 *    master slide and its sub-elements;
 *  - initializing text field content providers;
 *  - initializing the slide animation handler.
 *
 *  @param sMetaSlideId
 *      The string representing the id attribute of the meta-slide element.
 *  @param aMetaDoc
 *      The MetaDocument global object.
 */
function MetaSlide( sMetaSlideId, aMetaDoc )
{
    this.theDocument = document;
    this.id = sMetaSlideId;
    this.theMetaDoc = aMetaDoc;

    // We get a reference to the meta-slide element.
    this.element = this.theDocument.getElementById( this.id );
    assert( this.element,
            'MetaSlide: meta_slide element <' + this.id + '> not found.' );

    // We get a reference to the slide element.
    this.slideId = this.element.getAttributeNS( NSS['ooo'], aOOOAttrSlide );
    this.slideElement = this.theDocument.getElementById( this.slideId );
    assert( this.slideElement,
            'MetaSlide: slide element <' + this.slideId + '> not found.' );

    if( this.slideId !== 'dummy_slide' )
        this.nSlideNumber = parseInt( this.slideId.substr(2) );
    else
        this.nSlideNumber= -1;

    this.slideName = this.element.getAttributeNS( NSS['ooo'], aOOOAttrDisplayName );

    // Each slide element is double wrapped by <g> elements.
    // The outer <g> element is responsible for
    // the slide element visibility. In fact the visibility attribute has
    // to be set on the parent of the slide element and not directly on
    // the slide element. The reason is that in index mode each slide
    // rendered in a thumbnail view is targeted by a <use> element, however
    // when the visibility attribute is set directly on the referred slide
    // element its visibility is not overridden by the visibility attribute
    // defined by the targeting <use> element. The previous solution was,
    // when the user switched to index mode, to set up the visibility attribute
    // of all slides rendered in a thumbnail to 'visible'.
    // Obviously the slides were not really visible because the grid of
    // thumbnails was above them, anyway Firefox performance was really bad.
    // The workaround of setting up the visibility attribute on the slide
    // parent element let us to make visible a slide in a <use> element
    // even if the slide parent element visibility is set to 'hidden'.
    // The inner <g> element is used in order to append some element
    // before or after the slide, operation that can be needed for some
    // slide transition (e.g. fade through black). In this way we can
    // create a view of both the slide and the appended elements that turns out
    // to be useful for handling transition from the last to the first slide.
    this.aContainerElement = this.slideElement.parentNode;
    this.slideContainerId = this.aContainerElement.getAttribute( 'id' );
    this.aVisibilityStatusElement = this.aContainerElement.parentNode;

    // We get a reference to the draw page element, where all shapes specific
    // of this slide live.
    this.pageElement = getElementByClassName( this.slideElement, aPageClassName );
    assert( this.pageElement,
            'MetaSlide: page element <' + this.slideId + '> not found.' );

    // The slide custom background element and its id attribute.
    this.backgroundElement = getElementByClassName( this.pageElement, 'Background' );
    if( this.backgroundElement )
    {
        this.backgroundId = this.backgroundElement.getAttribute( 'id' );
    }

    // We initialize text fields
    this.initPlaceholderElements();

    // We initialize the MasterPage object that provides direct access to
    // the target master page element.
    this.masterPage = this.initMasterPage();

    // We check if the slide has a custom background which overrides the one of the targeted master page
    this.bHasCustomBackground = this.initHasCustomBackground();

    // We initialize visibility properties of the target master page elements.
    this.nAreMasterObjectsVisible     = this.initVisibilityProperty( aOOOAttrMasterObjectsVisibility,  VISIBLE );
    this.nIsBackgroundVisible         = this.initVisibilityProperty( aOOOAttrBackgroundVisibility,     VISIBLE );
    this.nIsPageNumberVisible         = this.initVisibilityProperty( aOOOAttrPageNumberVisibility,     HIDDEN );
    this.nIsDateTimeVisible           = this.initVisibilityProperty( aOOOAttrDateTimeVisibility,       VISIBLE );
    this.nIsFooterVisible             = this.initVisibilityProperty( aOOOAttrFooterVisibility,         VISIBLE );
    this.nIsHeaderVisible             = this.initVisibilityProperty( aOOOAttrHeaderVisibility,         VISIBLE );

    // This property tell us if the date/time field need to be updated
    // each time the slide is shown. It is initialized in
    // the initDateTimeFieldContentProvider method.
    this.bIsDateTimeVariable = undefined;

    // We initialize the objects responsible to provide the content to text field.
    this.aTextFieldContentProviderSet = {};
    this.aTextFieldContentProviderSet[aSlideNumberClassName]   = this.initSlideNumberFieldContentProvider();
    this.aTextFieldContentProviderSet[aDateTimeClassName]      = this.initDateTimeFieldContentProvider( aOOOAttrDateTimeField );
    this.aTextFieldContentProviderSet[aFooterClassName]        = this.initFixedTextFieldContentProvider( aOOOAttrFooterField );
    this.aTextFieldContentProviderSet[aHeaderClassName]        = this.initFixedTextFieldContentProvider( aOOOAttrHeaderField );
    this.aTextFieldContentProviderSet[aDateClassName]          = this.theMetaDoc.aCurrentDateProvider;
    this.aTextFieldContentProviderSet[aTimeClassName]          = this.theMetaDoc.aCurrentTimeProvider;
    this.aTextFieldContentProviderSet[aSlideNameClassName]     = new FixedTextProvider( this.slideName );

    // We init the slide duration when automatic slide transition is enabled
    this.fDuration = this.initSlideDuration();

    // We look for slide transition.
    this.aTransitionHandler = null;
    this.bHasTransition = this.initHasTransition() || true;
    if( this.bHasTransition )
    {
        this.aTransitionHandler = new SlideTransition( this.getSlideAnimationsRoot(), this.slideId );
    }

    // We initialize the SlideAnimationsHandler object
    this.aSlideAnimationsHandler = new SlideAnimations( aSlideShow.getContext() );
    this.aSlideAnimationsHandler.importAnimations( this.getSlideAnimationsRoot() );
    this.aSlideAnimationsHandler.parseElements();

    // this statement is used only for debugging
    // eslint-disable-next-line no-constant-condition
    if( false && this.aSlideAnimationsHandler.aRootNode )
        log( this.aSlideAnimationsHandler.aRootNode.info( true ) );

    // We collect text shapes included in this slide .
    this.aTextShapeSet = this.collectTextShapes();

    // We initialize hyperlinks
    this.aHyperlinkSet = this.initHyperlinks();

}

MetaSlide.prototype =
{
/*** public methods ***/

/** show
 *  Set the visibility property of the slide to 'inherit'
 *  and update the master page view.
 */
show : function()
{
    this.updateMasterPageView();
    this.aVisibilityStatusElement.setAttribute( 'visibility', 'inherit' );
},

/** hide
 *  Set the visibility property of the slide to 'hidden'.
 */
hide : function()
{
    this.aVisibilityStatusElement.setAttribute( 'visibility', 'hidden' );
},

/** updateMasterPageView
 *  On first call it creates a master page view element and insert it at
 *  the begin of the slide element. Moreover it updates the text fields
 *  included in the master page view.
 */
updateMasterPageView : function()
{
    // The master page view element is generated and attached on first time
    // the slide is shown.
    if( !this.aMasterPageView )
    {
        this.aMasterPageView = new MasterPageView( this );
        this.aMasterPageView.attachToSlide();
    }
    this.aMasterPageView.update();
},

/*** private methods ***/

// It handles a text field inserted on a slide, not on a master page.
initPlaceholderElements : function()
{
    var aPlaceholderList = getElementsByClassName(this.pageElement , 'PlaceholderText' );
    var i = 0;
    for( ; i < aPlaceholderList.length; ++i )
    {
        var aPlaceholderElem = aPlaceholderList[i];
        var sClass = aPlaceholderElem.getAttribute('class');
        var sFieldType = sClass.split(' ')[1];
        if( sFieldType ===  aDateClassName)
            aPlaceholderElem.textContent = new Date().toLocaleDateString();
        else if( sFieldType === aTimeClassName )
            aPlaceholderElem.textContent = new Date().toLocaleTimeString();
    }
},

initMasterPage : function()
{
    var sMasterPageId = this.element.getAttributeNS( NSS['ooo'], aOOOAttrMaster );

    // Check that the master page handler object has not already been
    // created by another slide that target the same master page.
    if( !this.theMetaDoc.aMasterPageSet.hasOwnProperty( sMasterPageId ) )
    {
        this.theMetaDoc.aMasterPageSet[ sMasterPageId ] = new MasterPage( sMasterPageId, this );

        // We initialize aTextFieldHandlerSet[ sMasterPageId ] to an empty
        // collection.
        this.theMetaDoc.aTextFieldHandlerSet[ sMasterPageId ] = {};
    }
    return this.theMetaDoc.aMasterPageSet[ sMasterPageId ];
},

initSlideDuration : function()
{
    var sSlideDuration = this.element.getAttributeNS( NSS['ooo'], aOOOAttrSlideDuration );
    if( sSlideDuration && sSlideDuration.length > 0 )
        return parseFloat( sSlideDuration );
    else
        return -1;
},

initHasTransition : function()
{
    var sHasTransition = this.element.getAttributeNS( NSS['ooo'], aOOOAttrHasTransition );
    return ( sHasTransition === 'true' );
},

initHasCustomBackground : function()
{
    var sHasCustomBackground = this.element.getAttributeNS( NSS['ooo'], aOOOAttrHasCustomBackground );
    return ( sHasCustomBackground === 'true' );
},

initVisibilityProperty : function( aVisibilityAttribute, nDefaultValue )
{
    var nVisibility = nDefaultValue;
    var sVisibility = getOOOAttribute( this.element, aVisibilityAttribute );
    if( sVisibility )
        nVisibility = aVisibilityValue[ sVisibility ];
    return nVisibility;
},

initSlideNumberFieldContentProvider : function()
{
    return this.theMetaDoc.aSlideNumberProvider;
},

initDateTimeFieldContentProvider : function( aOOOAttrDateTimeField )
{
    var sTextFieldId = getOOOAttribute( this.element, aOOOAttrDateTimeField );
    if( !sTextFieldId )  return null;

    var nLength = aOOOElemTextField.length + 1;
    var nIndex = parseInt(sTextFieldId.substring( nLength ) );
    if( typeof nIndex != 'number') return null;

    if( !this.theMetaDoc.aTextFieldContentProviderSet[ nIndex ] )
    {
        var aTextField;
        var aTextFieldElem = document.getElementById( sTextFieldId );
        var sClassName = getClassAttribute( aTextFieldElem );
        if( sClassName == 'FixedDateTimeField' )
        {
            aTextField = new FixedTextByElementProvider( aTextFieldElem );
            this.bIsDateTimeVariable = false;
        }
        else if( sClassName == 'VariableDateTimeField' )
        {
            aTextField = new CurrentDateTimeProvider( aTextFieldElem );
            this.bIsDateTimeVariable = true;
        }
        else
        {
            aTextField = null;
        }
        this.theMetaDoc.aTextFieldContentProviderSet[ nIndex ] = aTextField;
    }
    return this.theMetaDoc.aTextFieldContentProviderSet[ nIndex ];
},

initFixedTextFieldContentProvider : function( aOOOAttribute )
{
    var sTextFieldId = getOOOAttribute( this.element, aOOOAttribute );
    if( !sTextFieldId ) return null;

    var nLength = aOOOElemTextField.length + 1;
    var nIndex = parseInt( sTextFieldId.substring( nLength ) );
    if( typeof nIndex != 'number') return null;

    if( !this.theMetaDoc.aTextFieldContentProviderSet[ nIndex ] )
    {
        var aTextFieldElem = document.getElementById( sTextFieldId );
        this.theMetaDoc.aTextFieldContentProviderSet[ nIndex ]
            = new FixedTextByElementProvider( aTextFieldElem );
    }
    return this.theMetaDoc.aTextFieldContentProviderSet[ nIndex ];
},

collectTextShapes : function()
{
    var aTextShapeSet = [];
    var aTextShapeIndexElem = getElementByClassName( document, 'TextShapeIndex' );
    if( aTextShapeIndexElem )
    {
        var aIndexEntryList = getElementChildren( aTextShapeIndexElem );
        var i;
        for( i = 0; i < aIndexEntryList.length; ++i )
        {
            var sSlideId = getOOOAttribute( aIndexEntryList[i], 'slide' );
            if( sSlideId === this.slideId )
            {
                var sTextShapeIds = getOOOAttribute( aIndexEntryList[i], 'id-list' );
                if( sTextShapeIds )
                {
                    var aTextShapeIdSet =  sTextShapeIds.split( ' ' );
                    var j;
                    for( j = 0; j < aTextShapeIdSet.length; ++j )
                    {
                        var aTextShapeElem = document.getElementById( aTextShapeIdSet[j] );
                        if( aTextShapeElem )
                        {
                            aTextShapeSet.push( aTextShapeElem );
                        }
                        else
                        {
                            log( 'warning: MetaSlide.collectTextShapes: text shape with id <' + aTextShapeIdSet[j] + '> is not valid.'  );
                        }
                    }
                }
                break;
            }
        }
    }
    return aTextShapeSet;
},

initHyperlinks : function()
{
    var aHyperlinkSet = {};
    var i;
    for( i = 0; i < this.aTextShapeSet.length; ++i )
    {
        if( this.aTextShapeSet[i] )
        {
            var aHyperlinkIdList = getElementByClassName( this.aTextShapeSet[i], 'HyperlinkIdList' );
            if( aHyperlinkIdList )
            {
                var sHyperlinkIds = aHyperlinkIdList.textContent;
                if( sHyperlinkIds )
                {
                    var aHyperlinkIdSet = sHyperlinkIds.trim().split( ' ' );
                    var j;
                    for( j = 0; j < aHyperlinkIdSet.length; ++j )
                    {
                        var sId = aHyperlinkIdSet[j];
                        aHyperlinkSet[ sId ] = new HyperlinkElement( sId, this.aSlideAnimationsHandler.aEventMultiplexer );
                    }
                }
            }
        }
    }
    return aHyperlinkSet;
},

getSlideAnimationsRoot : function()
{
    return this.theMetaDoc.aSlideAnimationsMap[ this.slideId ];
}

}; // end MetaSlide prototype

function removeRedundantParagraphFromTextFieldShape( aObject )
{
    var aTextElem = getElementByClassName( aObject, 'SVGTextShape' );
    if( aTextElem )
    {
        var aPlaceholderElement = getElementsByClassName(aTextElem, 'PlaceholderText');
        if( aPlaceholderElement )
        {
            var aTextParagraphSet = getElementsByClassName(aTextElem, 'TextParagraph');
            // When the text field width is too small, the placeholder text spans several lines.
            // We remove all text lines but the first one which is used as a placeholder.
            // This is a workaround but it should work in the majority of cases.
            // A complete solution needs to support svg text wrapping.
            if( aTextParagraphSet.length > 1 )
            {
                var i = aTextParagraphSet.length;
                while( i > 1 )
                {
                    aTextElem.removeChild(aTextParagraphSet[i - 1]);
                    --i;
                }
            }
        }
    }
}

function getTextFieldType ( elem ) {
    var sFieldType = null;
    var sClass = elem.getAttribute('class');
    if( sClass === 'TextShape' )
    {
        var aPlaceholderElement = getElementByClassName(elem, 'PlaceholderText');
        if( aPlaceholderElement )
        {
            var sClassAttr = aPlaceholderElement.getAttribute('class');
            var classes = sClassAttr.split(' ');
            sFieldType = classes[1];
        }
    }
    return sFieldType;
}

function isTextFieldByClassName ( sClassName )
{
    return sClassName.indexOf( aDateTimeClassName ) == 0 || sClassName.indexOf( aFooterClassName ) == 0
        || sClassName.indexOf( aHeaderClassName ) == 0 || sClassName.indexOf( aSlideNumberClassName ) == 0
        || sClassName.indexOf( aDateClassName ) == 0 || sClassName.indexOf( aTimeClassName ) == 0
        || sClassName.indexOf( aSlideNameClassName ) == 0;
}

/** Class MasterPage
 *  This class gives direct access to a master page element and to the following
 *  elements included in the master page:
 *  - the background element,
 *  - the background objects group element,
 *  Moreover for each text field element a Placeholder object is created which
 *  manages the text field element itself.
 *
 *  The master page element structure is the following:
 *  <g class='Master_Slide'>
 *      <g class='Background'>
 *          background image
 *      </g>
 *      <g class='BackgroundObjects'>
 *          <g class='DateTime'>
 *              date/time placeholder
 *          </g>
 *          <g class='Header'>
 *              header placeholder
 *          </g>
 *          <g class='Footer'>
 *              footer placeholder
 *          </g>
 *          <g class='PageNumber'>
 *              slide number placeholder
 *          </g>
 *          shapes
 *      </g>
 *  </g>
 *
 *  @param sMasterPageId
 *      A string representing the value of the id attribute of the master page
 *      element to be handled.
 * @param aMetaSlide
 *     A meta slide having as master page the one with the passed id.
 */
function MasterPage( sMasterPageId, aMetaSlide )
{
    this.id = sMasterPageId;
    this.metaSlide = aMetaSlide;

    // The master page element to be handled.
    this.element = document.getElementById( this.id );
    assert( this.element,
            'MasterPage: master page element <' + this.id + '> not found.' );

    // The master page background element and its id attribute.
    this.background = getElementByClassName( this.element, 'Background' );
    if( this.background )
    {
        this.backgroundId = this.background.getAttribute( 'id' );
        this.backgroundVisibility = initVisibilityProperty( this.background );
    }
    else
    {
        this.backgroundId = '';
        log( 'MasterPage: the background element is not valid.' );
    }

    // The background objects group element that contains every element presents
    // on the master page except the background element.
    this.backgroundObjects = getElementByClassName( this.element, 'BackgroundObjects' );
    this.aBackgroundObjectSubGroupIdList = [];
    if( this.backgroundObjects )
    {
        this.backgroundObjectsId = this.backgroundObjects.getAttribute( 'id' );
        this.backgroundObjectsVisibility = initVisibilityProperty( this.backgroundObjects );

        if( this.backgroundObjectsVisibility != HIDDEN )
        {
            var aBackgroundObjectList = getElementChildren( this.backgroundObjects );
            var nFrom = 0;
            var nCount = 0;
            var nSubGroupId = 1;
            var sClass;
            var sId = '';
            var i = 0;
            for( ; i < aBackgroundObjectList.length; ++i )
            {
                var aObject = aBackgroundObjectList[i];
                removeRedundantParagraphFromTextFieldShape( aObject );
                sClass = null;
                var sFieldType = getTextFieldType( aObject );
                if( sFieldType && aObject.firstElementChild )
                {
                    var sObjId = aObject.firstElementChild.getAttribute( 'id' );
                    if( sObjId )
                    {
                         sClass = sFieldType + '.' + sObjId;
                         aObject.setAttribute('class', sClass);
                    }
                }
                if( !sClass )
                {
                    sClass = aBackgroundObjectList[i].getAttribute('class');
                    if( sClass === aDateTimeClassName || sClass === aFooterClassName
                        || sClass === aHeaderClassName || sClass === aSlideNumberClassName)
                    {
                        sClass += '.Default';
                        aObject.setAttribute('class', sClass);
                    }
                }
                if( !sClass || !isTextFieldByClassName( sClass ) )
                {
                    if( nCount === 0 )
                    {
                        nFrom = i;
                        sId = this.backgroundObjectsId + '.' + nSubGroupId;
                        ++nSubGroupId;
                        this.aBackgroundObjectSubGroupIdList.push( sId );
                    }
                    ++nCount;
                }
                else
                {
                    this.aBackgroundObjectSubGroupIdList.push( sClass );
                    if( nCount !== 0 )
                    {
                        createElementGroup( this.backgroundObjects, aBackgroundObjectList, nFrom, nCount, 'BackgroundObjectSubgroup', sId );
                        nCount = 0;
                    }
                }
            }
            if( nCount !== 0 )
            {
                createElementGroup( this.backgroundObjects, aBackgroundObjectList, nFrom, nCount, 'BackgroundObjectSubgroup', sId );
            }
        }
    }
    else
    {
        this.backgroundObjectsId = '';
        log( 'MasterPage: the background objects element is not valid.' );
    }

    // We populate the collection of placeholders.
    this.aPlaceholderShapeSet = {};
    this.initPlaceholderShapes();
}

MasterPage.prototype =
{
/*** private methods ***/

initPlaceholderShapes : function()
{
    var sClassName;
    var i = 0;
    for( ; i < this.aBackgroundObjectSubGroupIdList.length; ++i )
    {
        sClassName = this.aBackgroundObjectSubGroupIdList[i];
        if( isTextFieldByClassName( sClassName ) )
            this.aPlaceholderShapeSet[ sClassName ] = new PlaceholderShape( this, sClassName );
    }
}

}; // end MasterPage prototype

/** Class PlaceholderShape
 *  This class provides direct access to a text field element and
 *  to the embedded placeholder element.
 *  Moreover it set up the text adjustment and position for the placeholder
 *  element.
 *  Note: the text field element included in a master page is used only as
 *  a template element, it is cloned for each specific text content
 *  (see the TextFieldContentProvider class and its derived classes).
 *
 *  @param aMasterPage
 *      The master page object to which the text field to be handled belongs.
 *  @param sClassName
 *      A string representing the value of the class attribute of the text
 *      field element to be handled.
 */
function PlaceholderShape( aMasterPage, sClassName )
{
    this.masterPage = aMasterPage;
    this.className = sClassName;

    this.element = null;
    this.textElement = null;
    this.init();
}

/* public methods */
PlaceholderShape.prototype.isValid = function()
{
    return ( this.element && this.textElement );
};

/* private methods */

/** init
 *  In case a text field element of class type 'className' exists and such
 *  an element embeds a placeholder element, the text adjustment and position
 *  of the placeholder element is set up.
 */
PlaceholderShape.prototype.init = function()
{
    var aTextFieldElement = getElementByClassName( this.masterPage.backgroundObjects, this.className );
    if( aTextFieldElement )
    {
        var aTextElem = getElementByClassName( aTextFieldElement, 'SVGTextShape' );
        if( aTextElem )
        {
            var aPlaceholderElement = getElementByClassName(aTextElem, 'PlaceholderText');
            if( aPlaceholderElement )
            {
                // SVG 1.1 does not support text wrapping wrt a rectangle.
                // When a text shape contains a placeholder, setting up the position
                // of each text line doesn't work since the position is computed
                // before replacing the placeholder text.
                // Anyway each text shape has an invisible rectangle that can be
                // regarded as the text shape bounding box.
                // We exploit such a feature and the exported text adjust attribute
                // value in order to set up correctly the position and text
                // adjustment for the text shape content.
                // We assume that once the real value has been substituted to
                // the placeholder the resulting content is no more than a single line.
                // So we remove from <tspan> elements used for setting up the
                // position of text lines (class TextPosition) the 'x' and 'y' attribute.
                // In the general case we would need to implement a function
                // which is able to compute at which words the text shape content has
                // to be wrapped.
                var aSVGRectElem = getElementByClassName( aTextFieldElement, 'BoundingBox' );
                if( aSVGRectElem )
                {
                    var aRect = new Rectangle( aSVGRectElem );
                    var sTextAdjust = getOOOAttribute( aTextFieldElement, aOOOAttrTextAdjust );
                    // the bbox of the text shape is indeed a bit larger, there is a bit of internal padding
                    var nMargin = 250; // 1000th mm
                    var sTextAnchor, sX;
                    if( sTextAdjust == 'left' )
                    {
                        sTextAnchor = 'start';
                        sX = String( Math.trunc( aRect.left + nMargin ) );
                    }
                    else if( sTextAdjust == 'right' )
                    {
                        sTextAnchor = 'end';
                        sX = String( Math.trunc( aRect.right - nMargin ) );
                    }
                    else if( sTextAdjust == 'center' )
                    {
                        sTextAnchor = 'middle';
                        var nMiddle = ( aRect.left + aRect.right ) / 2;
                        sX = String( parseInt( String( nMiddle ) ) );
                    }
                    if( sTextAnchor )
                    {
                        aTextElem.setAttribute( 'text-anchor', sTextAnchor );
                        if( sX )
                            aTextElem.setAttribute( 'x', sX );

                        var aTSpanElements = getElementsByClassName( aTextElem, 'TextPosition' );
                        if( aTSpanElements )
                        {
                            var i = 0;
                            for( ; i < aTSpanElements.length; ++i )
                            {
                                var aTSpanElem = aTSpanElements[i];
                                aTSpanElem.removeAttribute( 'x' );
                                if( i !== 0 )
                                    aTSpanElem.removeAttribute( 'y' );
                            }
                        }
                    }
                }

                // date/time fields were not exported correctly when positioned chars are used
                if( this.masterPage.metaSlide.theMetaDoc.bIsUsePositionedChars )
                {
                    // We remove all text lines but the first one used as placeholder.
                    var aTextLineGroupElem = aPlaceholderElement.parentNode.parentNode;
                    if( aTextLineGroupElem )
                    {
                        // Just to be sure it is the element we are looking for.
                        var sFontFamilyAttr = aTextLineGroupElem.getAttribute( 'font-family' );
                        if( sFontFamilyAttr )
                        {
                            var aChildSet = getElementChildren( aTextLineGroupElem );
                            if( aChildSet.length > 1 )
                                var i = 1;
                            for( ; i < aChildSet.length; ++i )
                            {
                                aTextLineGroupElem.removeChild( aChildSet[i] );
                            }
                        }
                    }
                }
                this.textElement = aPlaceholderElement;
            }
        }
        this.element = aTextFieldElement;
    }
};

/** Class MasterPageView
 *  This class is used to creates a svg element of class MasterPageView and its
 *  sub-elements.
 *  It is also responsible for updating the content of the included text fields.
 *
 *  MasterPageView element structure:
 *
 *  <g class='MasterPageView'>
 *      <use class='Background'>               // reference to master page background element
 *      <g class='BackgroundObjects'>
 *          <use class='BackgroundObjectSubGroup'>     // reference to the group of shapes on the master page that are below text fields
 *          <g class='PageNumber'>                   // a cloned element
 *                  ...
 *          </g>
 *          <use class='DateTime'>                    // reference to a clone
 *          <use class='Footer'>
 *          <use class='Header'>
 *          <use class='BackgroundObjectSubGroup'>     // reference to the group of shapes on the master page that are above text fields
 *      </g>
 *  </g>
 *
 *  Sub-elements are present only if they are visible.
 *
 *  @param aMetaSlide
 *      The MetaSlide object managing the slide element that targets
 *      the master page view element created by an instance of MasterPageView.
 */
function MasterPageView( aMetaSlide )
{
    this.aMetaSlide = aMetaSlide;
    this.aSlideElement = aMetaSlide.slideElement;
    this.aPageElement = aMetaSlide.pageElement;
    this.aMasterPage = aMetaSlide.masterPage;
    this.aMPVElement = this.createElement();
    this.bIsAttached = false;
}

/*** public methods ***/

/** attachToSlide
 *  Prepend the master slide view element to the slide element.
 */
MasterPageView.prototype.attachToSlide = function()
{
    if( !this.bIsAttached )
    {
        var aInsertedElement = this.aSlideElement.insertBefore( this.aMPVElement, this.aPageElement );
        assert( aInsertedElement === this.aMPVElement,
                'MasterPageView.attachToSlide: aInsertedElement != this.aMPVElement' );

        this.bIsAttached = true;
    }
};

/** detachFromSlide
 *  Remove the master slide view element from the slide element.
 */
MasterPageView.prototype.detachFromSlide = function()
{
    if( this.bIsAttached )
    {
        this.aSlideElement.removeChild( this.aMPVElement );
        this.bIsAttached = false;
    }
};

/** update
 *  Update the content of text fields placed on the master page.
 */
MasterPageView.prototype.update = function()
{
    if( this.aDateTimeFieldHandler && this.aMetaSlide.bIsDateTimeVariable )
        this.aDateTimeFieldHandler.update();
};

/*** private methods ***/

MasterPageView.prototype.createElement = function()
{
    var theDocument = document;
    var aMasterPageViewElement = theDocument.createElementNS( NSS['svg'], 'g' );
    assert( aMasterPageViewElement,
            'MasterPageView.createElement: failed to create a master page view element.' );
    aMasterPageViewElement.setAttribute( 'class', 'MasterPageView' );

    // we place a white rect below any else element
    // that is also a workaround for some kind of slide transition
    // when the master page is empty
    var aWhiteRect = theDocument.createElementNS( NSS['svg'], 'rect' );
    var nWidthExt = WIDTH / 1000;
    var nHeightExt = HEIGHT / 1000;
    aWhiteRect.setAttribute( 'x', String( -nWidthExt / 2 ) );
    aWhiteRect.setAttribute( 'y', String( -nHeightExt / 2 ) );
    aWhiteRect.setAttribute( 'width', String( WIDTH + nWidthExt ) );
    aWhiteRect.setAttribute( 'height', String( HEIGHT + nHeightExt ) );
    aWhiteRect.setAttribute( 'fill', '#FFFFFF' );
    aMasterPageViewElement.appendChild( aWhiteRect );

    // init the Background element
    if( this.aMetaSlide.nIsBackgroundVisible )
    {
        var nBackgroundId = this.aMetaSlide.bHasCustomBackground ? this.aMetaSlide.backgroundId : this.aMasterPage.backgroundId;
        this.aBackgroundElement = theDocument.createElementNS( NSS['svg'], 'use' );
        this.aBackgroundElement.setAttribute( 'class', 'Background' );
        setNSAttribute( 'xlink', this.aBackgroundElement,
                        'href', '#' + nBackgroundId );

        // node linking
        aMasterPageViewElement.appendChild( this.aBackgroundElement );
    }

    // init the BackgroundObjects element
    if( this.aMetaSlide.nAreMasterObjectsVisible )
    {
        this.aBackgroundObjectsElement = theDocument.createElementNS( NSS['svg'], 'g' );
        this.aBackgroundObjectsElement.setAttribute( 'class', 'BackgroundObjects' );

        // clone and initialize text field elements
        var aBackgroundObjectSubGroupIdList = this.aMasterPage.aBackgroundObjectSubGroupIdList;
        this.aBackgroundSubGroupElementSet = [];
        var aPlaceholderShapeSet = this.aMasterPage.aPlaceholderShapeSet;
        var aTextFieldContentProviderSet = this.aMetaSlide.aTextFieldContentProviderSet;
        // where cloned elements are appended
        var aDefsElement = this.aMetaSlide.element.parentNode;
        var aTextFieldHandlerSet = this.aMetaSlide.theMetaDoc.aTextFieldHandlerSet;
        var sMasterSlideId = this.aMasterPage.id;

        var i = 0;
        var sId;
        for( ; i < aBackgroundObjectSubGroupIdList.length; ++i )
        {
            sId = aBackgroundObjectSubGroupIdList[i];
            if( sId.indexOf( aSlideNumberClassName ) == 0 )
            {
                // Slide Number Field
                // The cloned element is appended directly to the field group element
                // since there is no slide number field content shared between two slide
                // (because the slide number of two slide is always different).
                var nIsPageNumberVisible = sId === (aSlideNumberClassName + '.Default') ? this.aMetaSlide.nIsPageNumberVisible : true;
                if( aPlaceholderShapeSet[sId] &&
                    aPlaceholderShapeSet[sId].isValid() &&
                    nIsPageNumberVisible &&
                    aTextFieldContentProviderSet[aSlideNumberClassName] )
                {
                    var aSlideNumberFieldHandler =
                        new SlideNumberFieldHandler( aPlaceholderShapeSet[sId],
                            aTextFieldContentProviderSet[aSlideNumberClassName] );
                    aSlideNumberFieldHandler.update( this.aMetaSlide.nSlideNumber );
                    aSlideNumberFieldHandler.appendTo( this.aBackgroundObjectsElement );
                    if ( sId === aSlideNumberClassName + '.Default' )
                        this.aSlideNumberFieldHandler = aSlideNumberFieldHandler;
                }
            }
            else if( sId === aDateTimeClassName + '.Default' )
            {
                // DateTime field
                if( this.aMetaSlide.nIsDateTimeVisible )
                {
                    this.aDateTimeFieldHandler =
                        this.initTextFieldHandler( sId, aPlaceholderShapeSet,
                                                   aTextFieldContentProviderSet, aDefsElement,
                                                   aTextFieldHandlerSet, sMasterSlideId );
                }
            }
            else if( sId === aFooterClassName + '.Default' )
            {
                // Footer Field
                if( this.aMetaSlide.nIsFooterVisible )
                {
                    this.aFooterFieldHandler =
                        this.initTextFieldHandler( sId, aPlaceholderShapeSet,
                                                   aTextFieldContentProviderSet, aDefsElement,
                                                   aTextFieldHandlerSet, sMasterSlideId );
                }
            }
            else if( sId === aHeaderClassName + '.Default' )
            {
                // Header Field
                if( this.aMetaSlide.nIsHeaderVisible )
                {
                    this.aHeaderFieldHandler =
                        this.initTextFieldHandler( sId, aPlaceholderShapeSet,
                                                   aTextFieldContentProviderSet, aDefsElement,
                                                   aTextFieldHandlerSet, sMasterSlideId );
                }
            }
            else if( sId.indexOf( aDateTimeClassName ) == 0
                || sId.indexOf( aFooterClassName ) == 0
                || sId.indexOf( aHeaderClassName ) == 0
                || sId.indexOf( aDateClassName ) == 0
                || sId.indexOf( aTimeClassName ) == 0
                || sId.indexOf( aSlideNameClassName ) == 0 )
            {
                this.initTextFieldHandler( sId, aPlaceholderShapeSet,
                                           aTextFieldContentProviderSet, aDefsElement,
                                           aTextFieldHandlerSet, sMasterSlideId );
            }
            else
            {
                // init BackgroundObjectSubGroup elements
                var aBackgroundSubGroupElement = theDocument.createElementNS( NSS['svg'], 'use' );
                aBackgroundSubGroupElement.setAttribute( 'class', 'BackgroundObjectSubGroup' );
                setNSAttribute( 'xlink', aBackgroundSubGroupElement,
                                'href', '#' + sId );
                this.aBackgroundSubGroupElementSet.push( aBackgroundSubGroupElement );
                // node linking
                this.aBackgroundObjectsElement.appendChild( aBackgroundSubGroupElement );
            }

        }
        // node linking
        aMasterPageViewElement.appendChild( this.aBackgroundObjectsElement );
    }

    return aMasterPageViewElement;
};

MasterPageView.prototype.initTextFieldHandler =
    function( sId, aPlaceholderShapeSet, aTextFieldContentProviderSet,
              aDefsElement, aTextFieldHandlerSet, sMasterSlideId )
{
    var sRefId = null;
    var aTextFieldHandler = null;
    var sClassName = sId.split('.')[0];
    var aPlaceholderShape = aPlaceholderShapeSet[sId];
    var aTextFieldContentProvider = aTextFieldContentProviderSet[sClassName];
    if( aPlaceholderShape  && aPlaceholderShape.isValid()
        && aTextFieldContentProvider )
    {
        var sTextFiedHandlerKey = aTextFieldContentProvider.sId + '.' + sId;
        // We create only one single TextFieldHandler object (and so one only
        // text field clone) per master slide and text content.
        if ( !aTextFieldHandlerSet[ sMasterSlideId ][ sTextFiedHandlerKey ] )
        {
            aTextFieldHandlerSet[ sMasterSlideId ][ sTextFiedHandlerKey ] =
                new TextFieldHandler( aPlaceholderShape,
                                      aTextFieldContentProvider );
            aTextFieldHandler = aTextFieldHandlerSet[ sMasterSlideId ][ sTextFiedHandlerKey ];
            aTextFieldHandler.update();
            aTextFieldHandler.appendTo( aDefsElement );
        }
        else
        {
            aTextFieldHandler = aTextFieldHandlerSet[ sMasterSlideId ][ sTextFiedHandlerKey ];
        }
        sRefId = aTextFieldHandler.sId;
    }
    else if( aPlaceholderShape && aPlaceholderShape.element && aPlaceholderShape.element.firstElementChild
        && !aPlaceholderShape.textElement && !aTextFieldContentProvider )
    {
        sRefId = aPlaceholderShape.element.firstElementChild.getAttribute('id');
    }

    if( sRefId )
    {
        // We create a <use> element referring to the cloned text field and
        // append it to the field group element.
        var aTextFieldElement = document.createElementNS(NSS['svg'], 'use');
        aTextFieldElement.setAttribute('class', sClassName);
        setNSAttribute('xlink', aTextFieldElement,
            'href', '#' + sRefId);
        // node linking
        this.aBackgroundObjectsElement.appendChild( aTextFieldElement );
    }
    return aTextFieldHandler;
};

/** Class TextFieldHandler
 *  This class clone a text field field of a master page and set up
 *  the content of the cloned element on demand.
 *
 *  @param aPlaceholderShape
 *      A PlaceholderShape object that provides the text field to be cloned.
 *  @param aTextContentProvider
 *      A TextContentProvider object to which the actual content updating is
 *      demanded.
 */
function TextFieldHandler( aPlaceholderShape, aTextContentProvider )
{
    this.aPlaceHolderShape = aPlaceholderShape;
    this.aTextContentProvider = aTextContentProvider;
    assert( this.aTextContentProvider,
            'TextFieldHandler: text content provider not defined.' );
    this.sId = 'tf' + String( TextFieldHandler.getUniqueId() );
    // The cloned text field element to be handled.
    this.aTextFieldElement = null;
    // The actual <text> element where the field content has to be placed.
    this.aTextPlaceholderElement = null;
    this.cloneElement();
}

/*** private methods ***/

TextFieldHandler.CURR_UNIQUE_ID = 0;

TextFieldHandler.getUniqueId = function()
{
    ++TextFieldHandler.CURR_UNIQUE_ID;
    return TextFieldHandler.CURR_UNIQUE_ID;
};

TextFieldHandler.prototype.cloneElement = function()
{
    assert( this.aPlaceHolderShape && this.aPlaceHolderShape.isValid(),
            'TextFieldHandler.cloneElement: placeholder shape is not valid.' );
    // The cloned text field element.
    this.aTextFieldElement = this.aPlaceHolderShape.element.cloneNode( true /* deep clone */ );
    assert( this.aTextFieldElement,
            'TextFieldHandler.cloneElement: aTextFieldElement is not defined' );
    this.aTextFieldElement.setAttribute( 'id', this.sId );
    // Text field placeholder visibility is always set to 'hidden'.
    this.aTextFieldElement.removeAttribute( 'visibility' );
    // The actual <text> element where the field content has to be placed.
    this.aTextPlaceholderElement = getElementByClassName( this.aTextFieldElement, 'PlaceholderText' );
    assert( this.aTextPlaceholderElement,
            'TextFieldHandler.cloneElement: aTextPlaceholderElement is not defined' );
};

/*** public methods ***/

/** appendTo
 *  Append the cloned text field element to a svg element.
 *
 *  @param aParentNode
 *      The svg element to which the cloned text field has to be appended.
 */
TextFieldHandler.prototype.appendTo = function( aParentNode )
{
    if( !this.aTextFieldElement )
    {
        log( 'TextFieldHandler.appendTo: aTextFieldElement is not defined' );
        return;
    }
    if( !aParentNode )
    {
        log( 'TextFieldHandler.appendTo: parent node is not defined' );
        return;
    }

    aParentNode.appendChild( this.aTextFieldElement );
};

/** setTextContent
 *  Modify the content of the cloned text field.
 *
 *  @param sText
 *      A string representing the new content of the cloned text field.
 */
TextFieldHandler.prototype.setTextContent = function( sText )
{
    if( !this.aTextPlaceholderElement )
    {
        log( 'PlaceholderShape.setTextContent: text element is not valid in placeholder of type '
             + this.className + ' that belongs to master slide ' + this.masterPage.id );
        return;
    }
    this.aTextPlaceholderElement.textContent = sText;
};

/** update
 *  Update the content of the handled text field. The new content is provided
 *  directly from the TextContentProvider data member.
 */
TextFieldHandler.prototype.update = function()
{
    if( !this.aTextContentProvider )
        log('TextFieldHandler.update: text content provider not defined.');
    else
        this.aTextContentProvider.update( this );
};

/** SlideNumberFieldHandler
 *  This class clone the slide number field of a master page and set up
 *  the content of the cloned element on demand.
 *
 *  @param aPlaceholderShape
 *      A PlaceholderShape object that provides the slide number field
 *      to be cloned.
 *  @param aTextContentProvider
 *      A SlideNumberProvider object to which the actual content updating is
 *      demanded.
 */
function SlideNumberFieldHandler( aPlaceholderShape, aTextContentProvider )
{
    SlideNumberFieldHandler.superclass.constructor.call( this, aPlaceholderShape, aTextContentProvider );
}
extend( SlideNumberFieldHandler, TextFieldHandler );

/*** public methods ***/

/** update
 *  Update the content of the handled slide number field with the passed number.
 *
 * @param nPageNumber
 *      The number representing the new content of the slide number field.
 */
SlideNumberFieldHandler.prototype.update = function( nPageNumber )
{
    // The actual content updating is demanded to the related
    // SlideNumberProvider instance that have the needed info on
    // the numbering type.
    if( !this.aTextContentProvider )
        log('TextFieldHandler.update: text content provider not defined.');
    else
        this.aTextContentProvider.update( this, nPageNumber );
};


/******************************************************************************
 * Text Field Content Provider Class Hierarchy
 *
 * The following classes are responsible to format and set the text content
 * of text fields.
 *
 ******************************************************************************/

/** Class TextFieldContentProvider
 *  This class is the root abstract class of the hierarchy.
 *
 *  @param aTextFieldContentElement
 *      The svg element that contains the text content for one or more
 *      master slide text field.
 */
function TextFieldContentProvider()
{
    this.sId = TextFieldContentProvider.getUniqueId();
}

/*** private methods ***/

TextFieldContentProvider.CURR_UNIQUE_ID = 0;

TextFieldContentProvider.getUniqueId = function()
{
    ++TextFieldContentProvider.CURR_UNIQUE_ID;
    return TextFieldContentProvider.CURR_UNIQUE_ID;
};

/** Class FixedTextProvider
 *  This class handles text field with a fixed text.
 *  The text content is provided by the 'text' property.
 *
 *  @param aText
 *      a string containing the text to be substituted.
 */
function FixedTextProvider( aText )
{
    FixedTextProvider.superclass.constructor.call( this );
    this.text = aText;
}
extend( FixedTextProvider, TextFieldContentProvider );

/*** public methods ***/

/** update
 *  Set up the content of a fixed text field.
 *
 *  @param aFixedTextField
 *      An object that implement a setTextContent( String ) method in order
 *      to set the content of a given text field.
 */
FixedTextProvider.prototype.update = function( aFixedTextField )
{
    aFixedTextField.setTextContent( this.text );
};

/** Class FixedTextByElementProvider
 *  This class handles text field with a fixed text.
 *  The text content is provided by the 'text' property.
 *
 *  @param aTextFieldContentElement
 *      The svg element that contains the text content for one or more
 *      master slide text field.
 */
function FixedTextByElementProvider( aTextFieldContentElement )
{
    FixedTextByElementProvider.superclass.constructor.call( this, aTextFieldContentElement.textContent );
}
extend( FixedTextByElementProvider, FixedTextProvider );

/** Class CurrentDateTimeProvider
 *  Provide the text content to a date/time field by generating the current
 *  date/time in the format specified by the 'dateTimeFormat' property.
 *
 *  @param aTextFieldContentElement
 *      The svg element that contains the date/time format for one or more
 *      master slide date/time field.
 */
function CurrentDateTimeProvider( aTextFieldContentElement, sDateTimeFormat )
{
    CurrentDateTimeProvider.superclass.constructor.call( this, aTextFieldContentElement );
    if( aTextFieldContentElement )
        this.dateTimeFormat = getOOOAttribute( aTextFieldContentElement, aOOOAttrDateTimeFormat );
    else
    {
        this.dateTimeFormat = sDateTimeFormat;
    }
}
extend( CurrentDateTimeProvider, TextFieldContentProvider );

/*** public methods ***/

/** update
 *  Set up the content of a variable date/time field.
 *
 *  @param aDateTimeField
 *      An object that implement a setTextContent( String ) method in order
 *      to set the content of a given text field.
 */
CurrentDateTimeProvider.prototype.update = function( aDateTimeField )
{
    var sText = this.createDateTimeText();
    aDateTimeField.setTextContent( sText );
};

/*** private methods ***/

CurrentDateTimeProvider.prototype.createDateTimeText = function()
{
    // TODO handle date/time format
    var sDate;
    if( this.dateTimeFormat === '<date>' )
        sDate = new Date().toLocaleDateString();
    else if( this.dateTimeFormat === '<time>' )
        sDate = new Date().toLocaleTimeString();
    else
        sDate = new Date().toLocaleDateString();
    return sDate;
};

/** Class SlideNumberProvider
 *  Provides the text content to the related text field by generating
 *  the current page number in the given page numbering type.
 */
function SlideNumberProvider( nInitialSlideNumber, sPageNumberingType )
{
    SlideNumberProvider.superclass.constructor.call( this );
    this.nInitialSlideNumber = nInitialSlideNumber;
    this.pageNumberingType = sPageNumberingType;

}
extend( SlideNumberProvider, TextFieldContentProvider );

/*** public methods ***/

/** getNumberingType
 *
 *  @return
 *      The page numbering type.
 */
SlideNumberProvider.prototype.getNumberingType = function()
{
    return this.pageNumberingType;
};

/** update
 *  Set up the content of a slide number field.
 *
 *  @param aSlideNumberField
 *      An object that implement a setTextContent( String ) method in order
 *      to set the content of a given text field.
 *  @param nSlideNumber
 *      An integer representing the slide number.
 */

SlideNumberProvider.prototype.update = function( aSlideNumberField, nSlideNumber )
{
    if( nSlideNumber === undefined )
    {
        if( nCurSlide === undefined )
            nSlideNumber = this.nInitialSlideNumber;
        else
            nSlideNumber = nCurSlide + 1;
    }
    var sText = this.createSlideNumberText( nSlideNumber, this.getNumberingType() );
    aSlideNumberField.setTextContent( sText );
};

/*** private methods ***/

SlideNumberProvider.prototype.createSlideNumberText = function( nSlideNumber /*, sNumberingType*/ )
{
    // TODO handle page numbering type
    return String( nSlideNumber );
};




/********************************
 ** Slide Index Classes **
 ********************************/

/** Class SlideIndexPage **
 *  This class is responsible for handling the slide index page
 */
function SlideIndexPage()
{
    this.pageElementId = 'slide_index';
    this.pageBgColor = 'rgb(252,252,252)';
    this.pageElement = this.createPageElement();
    assert( this.pageElement, 'SlideIndexPage: pageElement is not valid' );
    this.indexColumns = INDEX_COLUMNS_DEFAULT;
    this.totalThumbnails = this.indexColumns * this.indexColumns;
    this.selectedSlideIndex = undefined;

    // set up layout parameters
    this.xSpacingFactor = 600/28000;
    this.ySpacingFactor = 450/21000;
    this.xSpacing = WIDTH * this.xSpacingFactor;
    this.ySpacing = HEIGHT * this.ySpacingFactor;
    this.halfBorderWidthFactor = ( 300/28000 ) * ( this.indexColumns / 3 );
    this.halfBorderWidth = WIDTH * this.halfBorderWidthFactor;
    this.borderWidth = 2 * this.halfBorderWidth;
    // the following formula is used to compute the slide shrinking factor:
    // scaleFactor = ( WIDTH - ( columns + 1 ) * xSpacing ) / ( columns * ( WIDTH + borderWidth ) )
    // indeed we can divide everything by WIDTH:
    this.scaleFactor = ( 1 - ( this.indexColumns + 1 ) * this.xSpacingFactor ) /
                            ( this.indexColumns * ( 1 + 2 * this.halfBorderWidthFactor ) );

    // We create a Thumbnail Border and Thumbnail MouseArea rectangle template that will be
    // used by every Thumbnail. The Mouse Area rectangle is used in order to trigger the
    // mouseover event properly even when the slide background is hidden.
    this.thumbnailMouseAreaTemplateId = 'thumbnail_mouse_area';
    this.thumbnailMouseAreaTemplateElement = null;
    this.thumbnailBorderTemplateId = 'thumbnail_border';
    this.thumbnailBorderTemplateElement = null;
    this.createTemplateElements();

    // Now we create the grid of thumbnails
    this.aThumbnailSet = new Array( this.totalThumbnails );
    for( var i = 0; i < this.totalThumbnails; ++i )
    {
        this.aThumbnailSet[i] = new Thumbnail( this, i );
        this.aThumbnailSet[i].updateView();
    }

    this.curThumbnailIndex = 0;
}


/* public methods */
SlideIndexPage.prototype.getTotalThumbnails = function()
{
    return this.totalThumbnails;
};

SlideIndexPage.prototype.show = function()
{
    this.pageElement.setAttribute( 'display', 'inherit' );
};

SlideIndexPage.prototype.hide = function()
{
    this.pageElement.setAttribute( 'display', 'none' );
};

/** setSelection
 *
 * Change the selected thumbnail from the current one to the thumbnail with index nIndex.
 *
 * @param nIndex - the thumbnail index
 */
SlideIndexPage.prototype.setSelection = function( nIndex )
{
    nIndex = getSafeIndex( nIndex, 0, this.getTotalThumbnails() - 1 );
    if( this.curThumbnailIndex != nIndex )
    {
        this.aThumbnailSet[ this.curThumbnailIndex ].unselect();
        this.aThumbnailSet[ nIndex ].select();
        this.curThumbnailIndex = nIndex;
    }
    this.selectedSlideIndex = this.aThumbnailSet[ nIndex ].slideIndex;
};

SlideIndexPage.prototype.createPageElement = function()
{
    var aPageElement = document.createElementNS( NSS['svg'], 'g' );
    aPageElement.setAttribute( 'id', this.pageElementId );
    aPageElement.setAttribute( 'display', 'none' );
    aPageElement.setAttribute( 'visibility', 'visible' );

    // the slide index page background
    var sPageBgColor = this.pageBgColor + ';';
    var aRectElement = document.createElementNS( NSS['svg'], 'rect' );
    aRectElement.setAttribute( 'x', 0 );
    aRectElement.setAttribute( 'y', 0 );
    aRectElement.setAttribute( 'width', WIDTH );
    aRectElement.setAttribute( 'height', HEIGHT );
    aRectElement.setAttribute( 'style', 'stroke:none;fill:' + sPageBgColor );

    aPageElement.appendChild( aRectElement );
    // The index page is appended after all slide elements
    // so when it is displayed it covers them all
    ROOT_NODE.appendChild( aPageElement );
    return( document.getElementById( this.pageElementId ) );
};

SlideIndexPage.prototype.createTemplateElements = function()
{
    // We define a Rect element as a template of thumbnail border for all slide-thumbnails.
    // The stroke color is defined individually by each thumbnail according to
    // its selection status.
    var aDefsElement = document.createElementNS( NSS['svg'], 'defs' );
    var aRectElement = document.createElementNS( NSS['svg'], 'rect' );
    aRectElement.setAttribute( 'id', this.thumbnailBorderTemplateId );
    aRectElement.setAttribute( 'x', -this.halfBorderWidth );
    aRectElement.setAttribute( 'y', -this.halfBorderWidth );
    aRectElement.setAttribute( 'rx', this.halfBorderWidth );
    aRectElement.setAttribute( 'ry', this.halfBorderWidth );
    aRectElement.setAttribute( 'width', WIDTH + this.halfBorderWidth );
    aRectElement.setAttribute( 'height', HEIGHT + this.halfBorderWidth );
    aRectElement.setAttribute( 'stroke-width', this.borderWidth );
    aRectElement.setAttribute( 'fill', 'none' );
    aDefsElement.appendChild( aRectElement );

    // We define a Rect element as a template of mouse area for triggering the mouseover event.
    // A copy is used by each thumbnail element.
    aRectElement = document.createElementNS( NSS['svg'], 'rect' );
    aRectElement.setAttribute( 'id', this.thumbnailMouseAreaTemplateId );
    aRectElement.setAttribute( 'x', 0 );
    aRectElement.setAttribute( 'y', 0 );
    aRectElement.setAttribute( 'width', WIDTH );
    aRectElement.setAttribute( 'height', HEIGHT );
    aRectElement.setAttribute( 'fill', this.pageBgColor );
    aDefsElement.appendChild( aRectElement );

    this.pageElement.appendChild( aDefsElement );

    this.thumbnailMouseAreaTemplateElement = document.getElementById( this.thumbnailMouseAreaTemplateId );
    this.thumbnailBorderTemplateElement = document.getElementById( this.thumbnailBorderTemplateId );
};

SlideIndexPage.prototype.decreaseNumberOfColumns  = function()
{
    this.setNumberOfColumns( this.indexColumns - 1 );
};

SlideIndexPage.prototype.increaseNumberOfColumns  = function()
{
    this.setNumberOfColumns( this.indexColumns + 1 );
};

SlideIndexPage.prototype.resetNumberOfColumns  = function()
{
    this.setNumberOfColumns( INDEX_COLUMNS_DEFAULT );
};

/** setNumberOfColumns
 *
 * Change the size of the thumbnail grid.
 *
 * @param nNumberOfColumns - the new number of columns/rows of the thumbnail grid
 */
SlideIndexPage.prototype.setNumberOfColumns  = function( nNumberOfColumns )
{
    if( this.indexColumns == nNumberOfColumns )  return;
    if( nNumberOfColumns < 2 || nNumberOfColumns > 6 ) return;

    var suspendHandle = ROOT_NODE.suspendRedraw(500);

    var nOldTotalThumbnails = this.totalThumbnails;
    this.indexColumns = nNumberOfColumns;
    this.totalThumbnails = nNumberOfColumns * nNumberOfColumns;

    this.aThumbnailSet[this.curThumbnailIndex].unselect();

    // if we decreased the number of used columns we remove the exceeding thumbnail elements
    var i;
    for( i = this.totalThumbnails; i < nOldTotalThumbnails; ++i )
    {
        this.aThumbnailSet[i].removeElement();
    }

    // if we increased the number of used columns we create the needed thumbnail objects
    for( i = nOldTotalThumbnails; i < this.totalThumbnails; ++i )
    {
        this.aThumbnailSet[i] = new Thumbnail( this, i );
    }

    // we set up layout parameters that depend on the number of columns
    this.halfBorderWidthFactor = ( 300/28000 ) * ( this.indexColumns / 3 );
    this.halfBorderWidth = WIDTH * this.halfBorderWidthFactor;
    this.borderWidth = 2 * this.halfBorderWidth;
    // scaleFactor = ( WIDTH - ( columns + 1 ) * xSpacing ) / ( columns * ( WIDTH + borderWidth ) )
    this.scaleFactor = ( 1 - ( this.indexColumns + 1 ) * this.xSpacingFactor ) /
                            ( this.indexColumns * ( 1 + 2 * this.halfBorderWidthFactor ) );

    // update the thumbnail border size
    var aRectElement = this.thumbnailBorderTemplateElement;
    aRectElement.setAttribute( 'x', -this.halfBorderWidth );
    aRectElement.setAttribute( 'y', -this.halfBorderWidth );
    aRectElement.setAttribute( 'rx', this.halfBorderWidth );
    aRectElement.setAttribute( 'ry', this.halfBorderWidth );
    aRectElement.setAttribute( 'width', WIDTH + this.halfBorderWidth );
    aRectElement.setAttribute( 'height', HEIGHT + this.halfBorderWidth );
    aRectElement.setAttribute( 'stroke-width', this.borderWidth );

    // now we update the displacement on the index page of each thumbnail (old and new)
    for( i = 0; i < this.totalThumbnails; ++i )
    {
        this.aThumbnailSet[i].updateView();
    }

    this.curThumbnailIndex = this.selectedSlideIndex % this.totalThumbnails;
    this.aThumbnailSet[this.curThumbnailIndex].select();

    // needed for forcing the indexSetPageSlide routine to update the INDEX_OFFSET
    INDEX_OFFSET = -1;
    indexSetPageSlide( this.selectedSlideIndex );

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


/** Class Thumbnail **
 *  This class handles a slide thumbnail.
 */
function Thumbnail( aSlideIndexPage, nIndex )
{
    this.container = aSlideIndexPage;
    this.index = nIndex;//= getSafeIndex( nIndex, 0, this.container.getTotalThumbnails() );
    this.pageElement = this.container.pageElement;
    this.thumbnailId = 'thumbnail' + this.index;
    this.thumbnailElement = this.createThumbnailElement();
    this.slideElement = getElementByClassName( this.thumbnailElement, 'Slide' );
    this.borderElement = getElementByClassName( this.thumbnailElement, 'Border' );
    this.mouseAreaElement = getElementByClassName( this.thumbnailElement, 'MouseArea' );
    this.aTransformSet = new Array( 3 );
    this.visibility = VISIBLE;
    this.isSelected = false;
}

/* static const class member */
Thumbnail.prototype.sNormalBorderColor = 'rgb(216,216,216)';
Thumbnail.prototype.sSelectionBorderColor = 'rgb(92,92,255)';

/* public methods */
Thumbnail.prototype.removeElement = function()
{
    if( this.thumbnailElement )
        this.container.pageElement.removeChild( this.thumbnailElement );
};

Thumbnail.prototype.show = function()
{
    if( this.visibility == HIDDEN )
    {
        this.thumbnailElement.setAttribute( 'display', 'inherit' );
        this.visibility = VISIBLE;
    }
};

Thumbnail.prototype.hide = function()
{
    if( this.visibility == VISIBLE )
    {
        this.thumbnailElement.setAttribute( 'display', 'none' );
        this.visibility = HIDDEN;
    }
};

Thumbnail.prototype.select = function()
{
    if( !this.isSelected )
    {
        this.borderElement.setAttribute( 'stroke', this.sSelectionBorderColor );
        this.isSelected = true;
    }
};

Thumbnail.prototype.unselect = function()
{
    if( this.isSelected )
    {
        this.borderElement.setAttribute( 'stroke', this.sNormalBorderColor );
        this.isSelected = false;
    }
};

/** updateView
 *
 *  This method updates the displacement of the thumbnail on the slide index page,
 *  the value of the row, column coordinates of the thumbnail in the grid, and
 *  the onmouseover property of the thumbnail element.
 *
 */
Thumbnail.prototype.updateView = function()
{
    this.column = this.index % this.container.indexColumns;
    this.row = ( this.index - this.column ) / this.container.indexColumns;
    this.halfBorderWidth = this.container.halfBorderWidth;
    this.borderWidth = this.container.borderWidth;
    this.width = ( WIDTH + this.borderWidth ) * this.container.scaleFactor;
    this.height = ( HEIGHT + this.borderWidth ) * this.container.scaleFactor;
    this.aTransformSet[2] = 'translate(' + this.halfBorderWidth + ' ' + this.halfBorderWidth + ')';
    this.aTransformSet[1] = 'scale(' + this.container.scaleFactor + ')';
    var sTransformAttrValue = this.computeTransform();
    this.thumbnailElement.setAttribute( 'transform', sTransformAttrValue );
    this.mouseAreaElement.setAttribute( 'onmouseover', 'theSlideIndexPage.aThumbnailSet[' + this.index  + '].onMouseOver()' );
};

/** update
 *
 * This method update the content of the thumbnail view
 *
 * @param nIndex - the index of the slide to be shown in the thumbnail
 */
Thumbnail.prototype.update = function( nIndex )
{
    if( this.slideIndex == nIndex )  return;

    var aMetaSlide = theMetaDoc.aMetaSlideSet[nIndex];
    aMetaSlide.updateMasterPageView();
    setNSAttribute( 'xlink', this.slideElement, 'href', '#' + aMetaSlide.slideId );
    this.slideIndex = nIndex;
};

Thumbnail.prototype.clear = function( )
{
    setNSAttribute( 'xlink', this.slideElement, 'href', '' );
};

/* private methods */
Thumbnail.prototype.createThumbnailElement = function()
{
    var aThumbnailElement = document.createElementNS( NSS['svg'], 'g' );
    aThumbnailElement.setAttribute( 'id', this.thumbnailId );
    aThumbnailElement.setAttribute( 'display', 'inherit' );

    var aSlideElement = document.createElementNS( NSS['svg'], 'use' );
    setNSAttribute( 'xlink', aSlideElement, 'href', '' );
    aSlideElement.setAttribute( 'class', 'Slide' );
    aThumbnailElement.appendChild( aSlideElement );

    var aMouseAreaElement = document.createElementNS( NSS['svg'], 'use' );
    setNSAttribute( 'xlink', aMouseAreaElement, 'href', '#' + this.container.thumbnailMouseAreaTemplateId );
    aMouseAreaElement.setAttribute( 'class', 'MouseArea' );
    aMouseAreaElement.setAttribute( 'opacity', 0.0 );
    aThumbnailElement.appendChild( aMouseAreaElement );

    var aBorderElement = document.createElementNS( NSS['svg'], 'use' );
    setNSAttribute( 'xlink', aBorderElement, 'href', '#' + this.container.thumbnailBorderTemplateId );
    aBorderElement.setAttribute( 'stroke', this.sNormalBorderColor );
    aBorderElement.setAttribute( 'class', 'Border' );
    aThumbnailElement.appendChild( aBorderElement );

    this.container.pageElement.appendChild( aThumbnailElement );
    return( document.getElementById( this.thumbnailId ) );
};

Thumbnail.prototype.computeTransform = function()
{
    var nXSpacing = this.container.xSpacing;
    var nYSpacing = this.container.ySpacing;

    var nXOffset = nXSpacing + ( this.width + nXSpacing ) * this.column;
    var nYOffset = nYSpacing + ( this.height + nYSpacing ) * this.row;

    this.aTransformSet[0] = 'translate(' + nXOffset + ' ' + nYOffset + ')';

    var sTransform = this.aTransformSet.join( ' ' );

    return sTransform;
};

Thumbnail.prototype.onMouseOver = function()
{
    if( ( currentMode == INDEX_MODE ) && ( this.container.curThumbnailIndex !=  this.index ) )
    {
        this.container.setSelection( this.index );
    }
};





/** Initialization function.
 *  The whole presentation is set-up in this function.
 */
function init()
{
    var VIEWBOX = ROOT_NODE.getAttribute('viewBox');

    if( VIEWBOX )
    {
        WIDTH = ROOT_NODE.viewBox.animVal.width;
        HEIGHT = ROOT_NODE.viewBox.animVal.height;
    }

    aSlideShow = new SlideShow();
    theMetaDoc =  new MetaDocument();
    aSlideShow.bIsEnabled = theMetaDoc.bIsAnimated;
    theSlideIndexPage = new SlideIndexPage();
    aSlideShow.displaySlide( theMetaDoc.nStartSlideNumber, false );

    // Allow slide switching with swipe gestures left
    // and right. Swiping up or down will exit the slideshow.
    var hammer = new Hammer(ROOT_NODE);
    hammer.on('swipeleft', function() {
        switchSlide(1, false);
    });
    hammer.on('swiperight', function() {
        switchSlide(-1, false);
    });
    hammer.get('swipe').set({ direction: Hammer.DIRECTION_ALL });
    hammer.on('swipeup', function() {
        aSlideShow.exitSlideShowInApp();
    });
    hammer.on('swipedown', function() {
        aSlideShow.exitSlideShowInApp();
    });
}

function presentationEngineStop(message)
{
    alert( message + '\nThe presentation engine will be stopped' );
    document.onkeydown = null;
    document.onkeypress = null;
    document.onclick = null;
    window.onmousewheel = null;
}

function assert( condition, message )
{
    if (!condition)
    {
        presentationEngineStop( message );
        if (typeof console == 'object')
            // eslint-disable-next-line no-console
            console.trace();
        throw new Error( message );
    }
}

function dispatchEffects(dir)
{
    // TODO to be implemented

    if( dir == 1 )
    {
        var bRet = aSlideShow.nextEffect();

        if( !bRet )
        {
            switchSlide( 1, false );
        }
    }
    else
    {
        switchSlide( dir, false );
    }
}

function skipAllEffects()
{
    var bRet = aSlideShow.skipAllEffects();
    if( !bRet )
    {
        switchSlide( 1, true );
    }
}

function skipEffects(dir)
{
    if( dir == 1 )
    {
        var bRet = aSlideShow.skipPlayingOrNextEffect();

        if( !bRet )
        {
            switchSlide( 1, true );
        }
    }
    else
    {
        switchSlide( dir, true );
    }
}

function switchSlide( nOffset, bSkipTransition )
{
    var nNextSlide = nCurSlide + nOffset;
    aSlideShow.displaySlide( nNextSlide, bSkipTransition );
}

/** Function to display the index sheet.
 *
 *  @param offsetNumber offset number
 */
function displayIndex( offsetNumber )
{
    var aMetaSlideSet = theMetaDoc.aMetaSlideSet;
    offsetNumber = getSafeIndex( offsetNumber, 0, aMetaSlideSet.length - 1 );

    var nTotalThumbnails = theSlideIndexPage.getTotalThumbnails();
    var nEnd = Math.min( offsetNumber + nTotalThumbnails, aMetaSlideSet.length);

    var aThumbnailSet = theSlideIndexPage.aThumbnailSet;
    var j = 0;
    for( var i = offsetNumber; i < nEnd; ++i, ++j )
    {
        aThumbnailSet[j].update( i );
        aThumbnailSet[j].show();
    }
    for( ; j < nTotalThumbnails; ++j )
    {
        aThumbnailSet[j].hide();
    }

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

/** Function to toggle between index and slide mode.
 */
function toggleSlideIndex()
{
    if( currentMode == SLIDE_MODE )
    {

        theMetaDoc.getCurrentSlide().hide();
        INDEX_OFFSET = -1;
        indexSetPageSlide( nCurSlide );
        theSlideIndexPage.show();
        currentMode = INDEX_MODE;
    }
    else if( currentMode == INDEX_MODE )
    {
        theSlideIndexPage.hide();
        var nNewSlide = theSlideIndexPage.selectedSlideIndex;

        aSlideShow.displaySlide( nNewSlide, true );
        currentMode = SLIDE_MODE;
    }
}

/** Function that exit from the index mode without changing the shown slide
 *
 */
function abandonIndexMode()
{
    theSlideIndexPage.selectedSlideIndex = nCurSlide;
    toggleSlideIndex();
}





/*********************************************************************************************
 *********************************************************************************************
 *********************************************************************************************

                                  ***** ANIMATION ENGINE *****

 *********************************************************************************************
 *********************************************************************************************
 *********************************************************************************************/






// helper functions


var CURR_UNIQUE_ID = 0;

function getUniqueId()
{
    ++CURR_UNIQUE_ID;
    return CURR_UNIQUE_ID;
}

function mem_fn( sMethodName )
{
    return  function( aObject )
    {
        var aMethod = aObject[ sMethodName ];
        if( aMethod )
            aMethod.call( aObject );
        else
            log( 'method sMethodName not found' );
    };
}

function bind( aObject, aMethod )
{
    return  function()
    {
        return aMethod.call( aObject, arguments[0] );
    };
}

function bind2( aFunction )
{
    if( !aFunction  )
        log( 'bind2: passed function is not valid.' );

    var aBoundArgList = arguments;

    var aResultFunction = null;

    switch( aBoundArgList.length )
    {
        case 1: aResultFunction = function()
                {
                    return aFunction.call( arguments[0], arguments[1],
                                           arguments[2], arguments[3],
                                           arguments[4] );
                };
                break;
        case 2: aResultFunction = function()
                {
                    return aFunction.call( aBoundArgList[1], arguments[0],
                                           arguments[1], arguments[2],
                                           arguments[3] );
                };
                break;
        case 3: aResultFunction = function()
                {
                    return aFunction.call( aBoundArgList[1], aBoundArgList[2],
                                           arguments[0], arguments[1],
                                           arguments[2] );
                };
                break;
        case 4: aResultFunction = function()
                {
                    return aFunction.call( aBoundArgList[1], aBoundArgList[2],
                                           aBoundArgList[3], arguments[0],
                                           arguments[1] );
                };
                break;
        case 5: aResultFunction = function()
                {
                    return aFunction.call( aBoundArgList[1], aBoundArgList[2],
                                           aBoundArgList[3], aBoundArgList[4],
                                           arguments[0] );
                };
                break;
        default:
            log( 'bind2: arity not handled.' );
    }

    return aResultFunction;
}

function getCurrentSystemTime()
{
    return ( new Date() ).getTime();
}

// eslint-disable-next-line no-unused-vars
function getSlideAnimationsRoot( sSlideId )
{
    return theMetaDoc.aSlideAnimationsMap[ sSlideId ];
}

/** This function return an array populated with all children nodes of the
 *  passed element that are elements
 *
 *  @param aElement   any XML element
 *
 *  @returns   Array
 *      an array that contains all children elements
 */
function getElementChildren( aElement )
{
    var aChildrenArray = [];

    var nSize = aElement.childNodes.length;

    for( var i = 0; i < nSize; ++i )
    {
        if( aElement.childNodes[i].nodeType == 1 )
            aChildrenArray.push( aElement.childNodes[i] );
    }

    return aChildrenArray;
}

function removeWhiteSpaces( str )
{
    if( !str )
        return '';

    var re = / */;
    var aSplitString = str.split( re );
    return aSplitString.join('');
}

function clamp( nValue, nMinimum, nMaximum )
{
    if( nValue < nMinimum )
    {
        return nMinimum;
    }
    else if( nValue > nMaximum )
    {
        return nMaximum;
    }
    else
    {
        return nValue;
    }
}

function makeMatrixString( a, b, c, d, e, f )
{
    var s = 'matrix(';
    s += a + ', ';
    s += b + ', ';
    s += c + ', ';
    s += d + ', ';
    s += e + ', ';
    s += f + ')';

    return s;
}

// eslint-disable-next-line no-unused-vars
function matrixToString( aSVGMatrix )
{
    return makeMatrixString( aSVGMatrix.a, aSVGMatrix.b, aSVGMatrix.c,
                             aSVGMatrix.d, aSVGMatrix.e, aSVGMatrix.f );
}




// Attribute Parsers

// eslint-disable-next-line no-unused-vars
function numberParser( sValue )
{
    if( typeof sValue !== 'string' )
        return undefined;
    if( sValue === '.' )
        return undefined;
    var reFloatNumber = /^[+-]?[0-9]*[.]?[0-9]*$/;

    if( reFloatNumber.test( sValue ) )
        return parseFloat( sValue );
    else
        return undefined;
}

function booleanParser( sValue )
{
    if( typeof sValue !== 'string' )
        return undefined;

    sValue = sValue.toLowerCase();
    if( sValue === 'true' )
        return true;
    else if( sValue === 'false' )
        return false;
    else
        return undefined;
}

function colorParser( sValue )
{
    if( typeof sValue !== 'string' )
        return undefined;

    // The following 3 color functions are used in evaluating sValue string
    // so don't remove them.

    // eslint-disable-next-line no-unused-vars
    function hsl( nHue, nSaturation, nLuminance )
    {
        return new HSLColor( nHue, nSaturation / 100, nLuminance / 100 );
    }

    // eslint-disable-next-line no-unused-vars
    function rgb( nRed, nGreen, nBlue )
    {
        return new RGBColor( nRed / 255, nGreen / 255, nBlue / 255 );
    }

    // eslint-disable-next-line no-unused-vars
    function prgb( nRed, nGreen, nBlue )
    {
        return new RGBColor( nRed / 100, nGreen / 100, nBlue / 100 );
    }

    var sCommaPattern = ' *[,] *';
    var sIntegerPattern = '[+-]?[0-9]+';
    var sHexDigitPattern = '[0-9A-Fa-f]';

    var sHexColorPattern = '#(' + sHexDigitPattern + '{2})('
                                + sHexDigitPattern + '{2})('
                                + sHexDigitPattern + '{2})';

    var sRGBIntegerPattern = 'rgb[(] *' + sIntegerPattern + sCommaPattern
                                        + sIntegerPattern + sCommaPattern
                                        + sIntegerPattern + ' *[)]';

    var sRGBPercentPattern = 'rgb[(] *' + sIntegerPattern + '%' + sCommaPattern
                                        + sIntegerPattern + '%' + sCommaPattern
                                        + sIntegerPattern + '%' + ' *[)]';

    var sHSLPercentPattern = 'hsl[(] *' + sIntegerPattern + sCommaPattern
                                        + sIntegerPattern + '%' + sCommaPattern
                                        + sIntegerPattern + '%' + ' *[)]';

    var reHexColor = new RegExp( sHexColorPattern );
    var reRGBInteger = new RegExp( sRGBIntegerPattern );
    var reRGBPercent = new RegExp( sRGBPercentPattern );
    var reHSLPercent = new RegExp( sHSLPercentPattern );

    if( reHexColor.test( sValue ) )
    {
        var aRGBTriple = reHexColor.exec( sValue );

        var nRed    = parseInt( aRGBTriple[1], 16 ) / 255;
        var nGreen  = parseInt( aRGBTriple[2], 16 ) / 255;
        var nBlue   = parseInt( aRGBTriple[3], 16 ) / 255;

        return new RGBColor( nRed, nGreen, nBlue );
    }
    else if( reHSLPercent.test( sValue ) )
    {
        sValue = sValue.replace( '%', '' ).replace( '%', '' );
        return eval( sValue );
    }
    else if( reRGBInteger.test( sValue ) )
    {
        return eval( sValue );
    }
    else if( reRGBPercent.test( sValue ) )
    {
        sValue = 'p' + sValue.replace( '%', '' ).replace( '%', '' ).replace( '%', '' );
        return eval( sValue );
    }
    else
    {
        return null;
    }
}



/**********************************************************************************************
 *      RGB and HSL color classes
 **********************************************************************************************/


function RGBColor( nRed, nGreen, nBlue )
{
    this.eColorSpace = COLOR_SPACE_RGB;
    // values in the [0,1] range
    this.nRed = nRed;
    this.nGreen = nGreen;
    this.nBlue = nBlue;
}


RGBColor.prototype.clone = function()
{
    return new RGBColor( this.nRed, this.nGreen, this.nBlue );
};

RGBColor.prototype.equal = function( aRGBColor )
{
    return ( this.nRed == aRGBColor.nRed ) &&
           ( this.nGreen == aRGBColor.nGreen ) &&
           ( this.nBlue == aRGBColor.nBlue );
};

RGBColor.prototype.add = function( aRGBColor )
{
    this.nRed += aRGBColor.nRed;
    this.nGreen += aRGBColor.nGreen;
    this.nBlue += aRGBColor.nBlue;
    return this;
};

RGBColor.prototype.scale = function( aT )
{
    this.nRed *= aT;
    this.nGreen *= aT;
    this.nBlue *= aT;
    return this;
};

RGBColor.clamp = function( aRGBColor )
{
    var aClampedRGBColor = new RGBColor( 0, 0, 0 );

    aClampedRGBColor.nRed   = clamp( aRGBColor.nRed, 0.0, 1.0 );
    aClampedRGBColor.nGreen = clamp( aRGBColor.nGreen, 0.0, 1.0 );
    aClampedRGBColor.nBlue  = clamp( aRGBColor.nBlue, 0.0, 1.0 );

    return aClampedRGBColor;
};

RGBColor.prototype.convertToHSL = function()
{
    var nRed   = clamp( this.nRed, 0.0, 1.0 );
    var nGreen = clamp( this.nGreen, 0.0, 1.0 );
    var nBlue  = clamp( this.nBlue, 0.0, 1.0 );

    var nMax = Math.max( nRed, nGreen, nBlue );
    var nMin = Math.min( nRed, nGreen, nBlue );
    var nDelta = nMax - nMin;

    var nLuminance  = ( nMax + nMin ) / 2.0;
    var nSaturation = 0.0;
    var nHue = 0.0;
    if( nDelta !== 0 )
    {
        nSaturation = ( nLuminance > 0.5 ) ?
                            ( nDelta / ( 2.0 - nMax - nMin) ) :
                            ( nDelta / ( nMax + nMin ) );

        if( nRed == nMax )
            nHue = ( nGreen - nBlue ) / nDelta;
        else if( nGreen == nMax )
            nHue = 2.0 + ( nBlue - nRed ) / nDelta;
        else if( nBlue == nMax )
            nHue = 4.0 + ( nRed - nGreen ) / nDelta;

        nHue *= 60.0;

        if( nHue < 0.0 )
            nHue += 360.0;
    }

    return new HSLColor( nHue, nSaturation, nLuminance );

};

RGBColor.prototype.toString = function( bClamped )
{
    var aRGBColor;
    if( bClamped )
    {
        aRGBColor = RGBColor.clamp( this );
    }
    else
    {
        aRGBColor = this;
    }

    var nRed = Math.round( aRGBColor.nRed * 255 );
    var nGreen = Math.round( aRGBColor.nGreen * 255 );
    var nBlue = Math.round( aRGBColor.nBlue * 255 );

    return ( 'rgb(' + nRed + ',' + nGreen + ',' + nBlue + ')' );
};

RGBColor.interpolate = function( aStartRGB , aEndRGB, nT )
{
    var aResult = aStartRGB.clone();
    var aTEndRGB = aEndRGB.clone();
    aResult.scale( 1.0 - nT );
    aTEndRGB.scale( nT );
    aResult.add( aTEndRGB );

    return aResult;
};




function HSLColor( nHue, nSaturation, nLuminance )
{
    this.eColorSpace = COLOR_SPACE_HSL;
    // Hue is in the [0,360[ range, Saturation and Luminance are in the [0,1] range
    this.nHue = nHue;
    this.nSaturation = nSaturation;
    this.nLuminance = nLuminance;

    this.normalizeHue();
}


HSLColor.prototype.clone = function()
{
    return new HSLColor( this.nHue, this.nSaturation, this.nLuminance );
};

HSLColor.prototype.equal = function( aHSLColor )
{
    return ( this.nHue == aHSLColor.nHue ) &&
           ( this.nSaturation += aHSLColor.nSaturation ) &&
           ( this.nLuminance += aHSLColor.nLuminance );
};

HSLColor.prototype.add = function( aHSLColor )
{
    this.nHue += aHSLColor.nHue;
    this.nSaturation += aHSLColor.nSaturation;
    this.nLuminance += aHSLColor.nLuminance;
    this.normalizeHue();
    return this;
};

HSLColor.prototype.scale = function( aT )
{
    this.nHue *= aT;
    this.nSaturation *= aT;
    this.nLuminance *= aT;
    this.normalizeHue();
    return this;
};

HSLColor.clamp = function( aHSLColor )
{
    var aClampedHSLColor = new HSLColor( 0, 0, 0 );

    aClampedHSLColor.nHue = aHSLColor.nHue % 360;
    if( aClampedHSLColor.nHue < 0 )
        aClampedHSLColor.nHue += 360;
    aClampedHSLColor.nSaturation = clamp( aHSLColor.nSaturation, 0.0, 1.0 );
    aClampedHSLColor.nLuminance = clamp( aHSLColor.nLuminance, 0.0, 1.0 );
};

HSLColor.prototype.normalizeHue = function()
{
    this.nHue = this.nHue % 360;
    if( this.nHue < 0 ) this.nHue += 360;
};

HSLColor.prototype.toString = function()
{
    return 'hsl(' + this.nHue.toFixed( 3 ) + ','
                  + this.nSaturation.toFixed( 3 ) + ','
                  + this.nLuminance.toFixed( 3 ) + ')';
};

HSLColor.prototype.convertToRGB = function()
{

    var nHue = this.nHue % 360;
    if( nHue < 0 ) nHue += 360;
    var nSaturation =  clamp( this.nSaturation, 0.0, 1.0 );
    var nLuminance = clamp( this.nLuminance, 0.0, 1.0 );


    if( nSaturation === 0 )
    {
        return new RGBColor( nLuminance, nLuminance, nLuminance );
    }

    var nVal1 = ( nLuminance <= 0.5 ) ?
                        ( nLuminance * (1.0 + nSaturation) ) :
                        ( nLuminance + nSaturation - nLuminance * nSaturation );

    var nVal2 = 2.0 * nLuminance - nVal1;

    var nRed    = HSLColor.hsl2rgbHelper( nVal2, nVal1, nHue + 120 );
    var nGreen  = HSLColor.hsl2rgbHelper( nVal2, nVal1, nHue );
    var nBlue   = HSLColor.hsl2rgbHelper( nVal2, nVal1, nHue - 120 );

    return new RGBColor( nRed, nGreen, nBlue );
};

HSLColor.hsl2rgbHelper = function( nValue1, nValue2, nHue )
{
    nHue = nHue % 360;
    if( nHue < 0 )
        nHue += 360;

    if( nHue < 60.0 )
        return nValue1 + ( nValue2 - nValue1 ) * nHue / 60.0;
    else if( nHue < 180.0 )
        return nValue2;
    else if( nHue < 240.0 )
        return ( nValue1 + ( nValue2 - nValue1 ) * ( 240.0 - nHue ) / 60.0 );
    else
        return nValue1;
};

HSLColor.interpolate = function( aFrom, aTo, nT, bCCW )
{
    var nS = 1.0 - nT;

    var nHue = 0.0;
    if( aFrom.nHue <= aTo.nHue && !bCCW )
    {
        // interpolate hue clockwise. That is, hue starts at
        // high values and ends at low ones. Therefore, we
        // must 'cross' the 360 degrees and start at low
        // values again (imagine the hues to lie on the
        // circle, where values above 360 degrees are mapped
        // back to [0,360)).
        nHue = nS * (aFrom.nHue + 360.0) + nT * aTo.nHue;
    }
    else if( aFrom.nHue > aTo.nHue && bCCW )
    {
        // interpolate hue counter-clockwise. That is, hue
        // starts at high values and ends at low
        // ones. Therefore, we must 'cross' the 360 degrees
        // and start at low values again (imagine the hues to
        // lie on the circle, where values above 360 degrees
        // are mapped back to [0,360)).
        nHue = nS * aFrom.nHue + nT * (aTo.nHue + 360.0);
    }
    else
    {
        // interpolate hue counter-clockwise. That is, hue
        // starts at low values and ends at high ones (imagine
        // the hue value as degrees on a circle, with
        // increasing values going counter-clockwise)
        nHue = nS * aFrom.nHue + nT * aTo.nHue;
    }

    var nSaturation = nS * aFrom.nSaturation + nT * aTo.nSaturation;
    var nLuminance = nS * aFrom.nLuminance + nT * aTo.nLuminance;

    return new HSLColor( nHue, nSaturation, nLuminance );
};



/**********************************************************************************************
 *      SVGMatrix extensions
 **********************************************************************************************/

var SVGIdentityMatrix = document.documentElement.createSVGMatrix();

SVGMatrix.prototype.setToIdentity = function()
{
    this.a = this.d = 1;
    this.b = this.c = this.d = this.e = 0;
};

SVGMatrix.prototype.setToRotationAroundPoint = function( nX, nY, nAngle )
{
    // convert to radians
    nAngle = Math.PI * nAngle / 180;
    var nSin = Math.sin( nAngle );
    var nCos = Math.cos( nAngle );

    this.a = nCos; this.c = -nSin; this.e = nX * (1 - nCos) + nY * nSin;
    this.b = nSin; this.d =  nCos; this.f = nY * (1 - nCos) - nX * nSin;
};



/**********************************************************************************************
 *      SVGPath extensions
 **********************************************************************************************/

/** SVGPathElement.prependPath
 *  Merge the two path together by inserting the passed path before this one.
 *
 *  @param aPath
 *      An object of type SVGPathElement to be prepended.
 */
SVGPathElement.prototype.prependPath = function( aPath )
{
    var sPathData = aPath.getAttribute( 'd' );
    sPathData += ( ' ' + this.getAttribute( 'd' ) );
    this.setAttribute( 'd', sPathData );
};

/** SVGPathElement.appendPath
 *  Merge the two path together by inserting the passed path after this one.
 *
 *  @param aPath
 *      An object of type SVGPathElement to be appended.
 */
SVGPathElement.prototype.appendPath = function( aPath )
{
    var sPathData = this.getAttribute( 'd' );
    sPathData += ( ' ' + aPath.getAttribute( 'd' ) );
    this.setAttribute( 'd', sPathData );
};

/** flipOnYAxis
 *  Flips the SVG Path element along y-axis.
 *
 *  @param aPath
 *      An object of type SVGPathElement to be flipped.
 */
function flipOnYAxis( aPath )
{
    var aPolyPath = aPath.cloneNode(true);
    var aTransform = document.documentElement.createSVGMatrix();
    aTransform.a = -1;
    aTransform.e = 1;
    aPolyPath.matrixTransform(aTransform);
    return aPolyPath;
}

/** flipOnXAxis
 *  Flips the SVG Path element along x-axis
 *
 *  @param aPath
 *      An object of type SVGPathElement to be flipped
 */
function flipOnXAxis( aPath )
{
    var aPolyPath = aPath.cloneNode(true);
    var aTransform = document.documentElement.createSVGMatrix();
    aTransform.d = -1;
    aTransform.f = 1;
    aPolyPath.matrixTransform(aTransform);
    return aPolyPath;
}

/** SVGPathElement.matrixTransform
 *  Apply the transformation defined by the passed matrix to the referenced
 *  svg <path> element.
 *  After the transformation 'this' element is modified in order to reference
 *  the transformed path.
 *
 *  @param aSVGMatrix
 *      An SVGMatrix instance.
 */
SVGPathElement.prototype.matrixTransform = function( aSVGMatrix )
{
    if( SVGPathSegList.prototype.matrixTransform )
    {
        this.pathSegList.matrixTransform( aSVGMatrix );
        return;
    }

    var aPathSegList = this.pathSegList;
    var nLength = aPathSegList.numberOfItems;
    var i;
    for( i = 0; i < nLength; ++i )
    {
        aPathSegList.getItem( i ).matrixTransform( aSVGMatrix );
    }
};

/** SVGPathElement.changeOrientation
 *  Invert the path orientation by inverting the path command list.
 *
 */
SVGPathElement.prototype.changeOrientation = function()
{
    var aPathSegList = this.pathSegList;
    var nLength = aPathSegList.numberOfItems;
    if( nLength == 0 ) return;

    if( SVGPathSegList.prototype.changeOrientation )
    {
        aPathSegList.changeOrientation();
        return;
    }

    var nCurrentX = 0;
    var nCurrentY = 0;

    var aPathSeg = aPathSegList.getItem( 0 );
    if( aPathSeg.pathSegTypeAsLetter == 'M' )
    {
        nCurrentX = aPathSeg.x;
        nCurrentY = aPathSeg.y;
        aPathSegList.removeItem( 0 );
        --nLength;
    }

    var i;
    for( i = 0; i < nLength; ++i )
    {
        aPathSeg = aPathSegList.getItem( i );
        var aPoint = aPathSeg.changeOrientation( nCurrentX, nCurrentY );
        nCurrentX = aPoint.x;
        nCurrentY = aPoint.y;
    }


    for( i = nLength - 2; i >= 0; --i )
    {
        aPathSeg = aPathSegList.removeItem( i );
        aPathSegList.appendItem( aPathSeg );
    }

    var aMovePathSeg = this.createSVGPathSegMovetoAbs( nCurrentX, nCurrentY );
    aPathSegList.insertItemBefore( aMovePathSeg, 0 );

};

/** matrixTransform and changeOrientation
 *  We implement these methods for each path segment type still present
 *  after the path normalization (M, L, Q, C).
 *
 *  Note: Opera doesn't have any SVGPathSeg* class and rises an error.
 *  We exploit this fact for providing a different implementation.
 */


try
{   // Firefox, Google Chrome, Internet Explorer, Safari.

    SVGPathSegMovetoAbs.prototype.matrixTransform = function( aSVGMatrix )
    {
        SVGPathMatrixTransform( this, aSVGMatrix );
    };

    SVGPathSegLinetoAbs.prototype.matrixTransform = function( aSVGMatrix )
    {
        SVGPathMatrixTransform( this, aSVGMatrix );
    };

    SVGPathSegCurvetoQuadraticAbs.prototype.matrixTransform = function( aSVGMatrix )
    {
        SVGPathMatrixTransform( this, aSVGMatrix );
        var nX = this.x1;
        this.x1 = aSVGMatrix.a * nX + aSVGMatrix.c * this.y1 + aSVGMatrix.e;
        this.y1 = aSVGMatrix.b * nX + aSVGMatrix.d * this.y1 + aSVGMatrix.f;
    };

    SVGPathSegCurvetoCubicAbs.prototype.matrixTransform = function( aSVGMatrix )
    {
        SVGPathMatrixTransform( this, aSVGMatrix );
        var nX = this.x1;
        this.x1 = aSVGMatrix.a * nX + aSVGMatrix.c * this.y1 + aSVGMatrix.e;
        this.y1 = aSVGMatrix.b * nX + aSVGMatrix.d * this.y1 + aSVGMatrix.f;
        nX = this.x2;
        this.x2 = aSVGMatrix.a * nX + aSVGMatrix.c * this.y2 + aSVGMatrix.e;
        this.y2 = aSVGMatrix.b * nX + aSVGMatrix.d * this.y2 + aSVGMatrix.f;
    };


    SVGPathSegMovetoAbs.prototype.changeOrientation = function( nCurrentX, nCurrentY )
    {
        var aPoint = { x: this.x, y: this.y };
        this.x = nCurrentX;
        this.y = nCurrentY;
        return aPoint;
    };

    SVGPathSegLinetoAbs.prototype.changeOrientation = function( nCurrentX, nCurrentY )
    {
        var aPoint = { x: this.x, y: this.y };
        this.x = nCurrentX;
        this.y = nCurrentY;
        return aPoint;
    };

    SVGPathSegCurvetoQuadraticAbs.prototype.changeOrientation = function( nCurrentX, nCurrentY )
    {
        var aPoint = { x: this.x, y: this.y };
        this.x = nCurrentX;
        this.y = nCurrentY;
        return aPoint;
    };

    SVGPathSegCurvetoCubicAbs.prototype.changeOrientation = function( nCurrentX, nCurrentY )
    {
        var aPoint = { x: this.x, y: this.y };
        this.x = nCurrentX;
        this.y = nCurrentY;
        var nX = this.x1;
        this.x1 = this.x2;
        this.x2 = nX;
        var nY = this.y1;
        this.y1 = this.y2;
        this.y2 = nY;
        return aPoint;
    };

}
catch( e )
{   // Opera

    if( e.name == 'ReferenceError' )
    {
        SVGPathSeg.prototype.matrixTransform = function( aSVGMatrix )
        {
            var nX;
            switch( this.pathSegTypeAsLetter )
            {
                case 'C':
                    nX = this.x2;
                    this.x2 = aSVGMatrix.a * nX + aSVGMatrix.c * this.y2 + aSVGMatrix.e;
                    this.y2 = aSVGMatrix.b * nX + aSVGMatrix.d * this.y2 + aSVGMatrix.f;
                // fall through intended
                case 'Q':
                    nX = this.x1;
                    this.x1 = aSVGMatrix.a * nX + aSVGMatrix.c * this.y1 + aSVGMatrix.e;
                    this.y1 = aSVGMatrix.b * nX + aSVGMatrix.d * this.y1 + aSVGMatrix.f;
                // fall through intended
                case 'M':
                case 'L':
                    SVGPathMatrixTransform( this, aSVGMatrix );
                    break;
                default:
                    log( 'SVGPathSeg.matrixTransform: unexpected path segment type: '
                             + this.pathSegTypeAsLetter );
            }
        };

        SVGPathSeg.prototype.changeOrientation = function( nCurrentX, nCurrentY )
        {
            switch( this.pathSegTypeAsLetter )
            {
                case 'C':
                    var nX = this.x1;
                    this.x1 = this.x2;
                    this.x2 = nX;
                    var nY = this.y1;
                    this.y1 = this.y2;
                    this.y2 = nY;
                // fall through intended
                case 'M':
                case 'L':
                case 'Q':
                    var aPoint = { x: this.x, y: this.y };
                    this.x = nCurrentX;
                    this.y = nCurrentY;
                    return aPoint;
                default:
                    log( 'SVGPathSeg.changeOrientation: unexpected path segment type: '
                             + this.pathSegTypeAsLetter );
                    return null;
            }
        }
    }
    else throw e;
}

function SVGPathMatrixTransform( aPath, aSVGMatrix )
{
    var nX = aPath.x;
    aPath.x = aSVGMatrix.a * nX + aSVGMatrix.c * aPath.y + aSVGMatrix.e;
    aPath.y = aSVGMatrix.b * nX + aSVGMatrix.d * aPath.y + aSVGMatrix.f;
}


/**********************************************************************************************
 *      simple PriorityQueue
 **********************************************************************************************/

function PriorityQueue( aCompareFunc )
{
    this.aSequence = [];
    this.aCompareFunc = aCompareFunc;
}

PriorityQueue.prototype.clone = function()
{
    var aCopy = new PriorityQueue( this.aCompareFunc );
    var src = this.aSequence;
    var dest = [];
    var i, l;
    for( i = 0, l = src.length; i < l; ++i )
    {
        if( i in src )
        {
            dest.push( src[i] );
        }
    }
    aCopy.aSequence = dest;
    return aCopy;
};

PriorityQueue.prototype.top = function()
{
    return this.aSequence[this.aSequence.length - 1];
};

PriorityQueue.prototype.isEmpty = function()
{
    return ( this.aSequence.length === 0 );
};

PriorityQueue.prototype.push = function( aValue )
{
    this.aSequence.unshift( aValue );
    this.aSequence.sort(this.aCompareFunc);
};

PriorityQueue.prototype.clear = function()
{
    this.aSequence = [];
};

PriorityQueue.prototype.pop = function()
{
    return this.aSequence.pop();
};




/**********************************************************************************************
 *      AnimationNode Class Hierarchy
 **********************************************************************************************/



// Node Types
var ANIMATION_NODE_CUSTOM               = 0;
var ANIMATION_NODE_PAR                  = 1;
var ANIMATION_NODE_SEQ                  = 2;
var ANIMATION_NODE_ITERATE              = 3;
var ANIMATION_NODE_ANIMATE              = 4;
var ANIMATION_NODE_SET                  = 5;
var ANIMATION_NODE_ANIMATEMOTION        = 6;
var ANIMATION_NODE_ANIMATECOLOR         = 7;
var ANIMATION_NODE_ANIMATETRANSFORM     = 8;
var ANIMATION_NODE_TRANSITIONFILTER     = 9;
var ANIMATION_NODE_AUDIO                = 10;
var ANIMATION_NODE_COMMAND              = 11;

var aAnimationNodeTypeInMap = {
    'par'               : ANIMATION_NODE_PAR,
    'seq'               : ANIMATION_NODE_SEQ,
    'iterate'           : ANIMATION_NODE_ITERATE,
    'animate'           : ANIMATION_NODE_ANIMATE,
    'set'               : ANIMATION_NODE_SET,
    'animatemotion'     : ANIMATION_NODE_ANIMATEMOTION,
    'animatecolor'      : ANIMATION_NODE_ANIMATECOLOR,
    'animatetransform'  : ANIMATION_NODE_ANIMATETRANSFORM,
    'transitionfilter'  : ANIMATION_NODE_TRANSITIONFILTER,
    'audio'             : ANIMATION_NODE_AUDIO,
    'command'           : ANIMATION_NODE_COMMAND
};




function getAnimationElementType( aElement )
{
    var sName = aElement.localName.toLowerCase();

    if( sName && aAnimationNodeTypeInMap[ sName ] )
        return aAnimationNodeTypeInMap[ sName ];
    else
        return ANIMATION_NODE_CUSTOM;
}





// Node States
var INVALID_NODE                = 0;
var UNRESOLVED_NODE             = 1;
var RESOLVED_NODE               = 2;
var ACTIVE_NODE                 = 4;
var FROZEN_NODE                 = 8;
var ENDED_NODE                  = 16;

function getNodeStateName( eNodeState )
{
    switch( eNodeState )
    {
        case INVALID_NODE:
            return 'INVALID';
        case UNRESOLVED_NODE:
            return 'UNRESOLVED';
        case RESOLVED_NODE:
            return 'RESOLVED';
        case ACTIVE_NODE:
            return 'ACTIVE';
        case FROZEN_NODE:
            return 'FROZEN';
        case ENDED_NODE:
            return 'ENDED';
        default:
            return 'UNKNOWN';
    }
}


// Impress Node Types
var IMPRESS_DEFAULT_NODE                    = 0;
var IMPRESS_ON_CLICK_NODE                   = 1;
var IMPRESS_WITH_PREVIOUS_NODE              = 2;
var IMPRESS_AFTER_PREVIOUS_NODE             = 3;
var IMPRESS_MAIN_SEQUENCE_NODE              = 4;
var IMPRESS_TIMING_ROOT_NODE                = 5;
var IMPRESS_INTERACTIVE_SEQUENCE_NODE       = 6;

var aImpressNodeTypeInMap = {
    'on-click'                  : IMPRESS_ON_CLICK_NODE,
    'with-previous'             : IMPRESS_WITH_PREVIOUS_NODE,
    'after-previous'            : IMPRESS_AFTER_PREVIOUS_NODE,
    'main-sequence'             : IMPRESS_MAIN_SEQUENCE_NODE,
    'timing-root'               : IMPRESS_TIMING_ROOT_NODE,
    'interactive-sequence'      : IMPRESS_INTERACTIVE_SEQUENCE_NODE
};

var aImpressNodeTypeOutMap = [ 'default', 'on-click', 'with-previous', 'after-previous',
                            'main-sequence', 'timing-root', 'interactive-sequence' ];


// Preset Classes
var aPresetClassInMap = {};


// Preset Ids
var aPresetIdInMap = {};


// Restart Modes
var RESTART_MODE_DEFAULT            = 0;
var RESTART_MODE_INHERIT            = 0; // eslint-disable-line no-unused-vars
var RESTART_MODE_ALWAYS             = 1;
var RESTART_MODE_WHEN_NOT_ACTIVE    = 2;
var RESTART_MODE_NEVER              = 3;

var aRestartModeInMap = {
    'inherit'       : RESTART_MODE_DEFAULT,
    'always'        : RESTART_MODE_ALWAYS,
    'whenNotActive' : RESTART_MODE_WHEN_NOT_ACTIVE,
    'never'         : RESTART_MODE_NEVER
};

var aRestartModeOutMap = [ 'inherit','always', 'whenNotActive', 'never' ];


// Fill Modes
var FILL_MODE_DEFAULT           = 0;
var FILL_MODE_INHERIT           = 0; // eslint-disable-line no-unused-vars
var FILL_MODE_REMOVE            = 1;
var FILL_MODE_FREEZE            = 2;
var FILL_MODE_HOLD              = 3;
var FILL_MODE_TRANSITION        = 4;
var FILL_MODE_AUTO              = 5;

var aFillModeInMap = {
    'inherit'       : FILL_MODE_DEFAULT,
    'remove'        : FILL_MODE_REMOVE,
    'freeze'        : FILL_MODE_FREEZE,
    'hold'          : FILL_MODE_HOLD,
    'transition'    : FILL_MODE_TRANSITION,
    'auto'          : FILL_MODE_AUTO
};

var aFillModeOutMap = [ 'inherit', 'remove', 'freeze', 'hold', 'transition', 'auto' ];


// Additive Modes
var ADDITIVE_MODE_UNKNOWN       = 0; // eslint-disable-line no-unused-vars
var ADDITIVE_MODE_BASE          = 1;
var ADDITIVE_MODE_SUM           = 2;
var ADDITIVE_MODE_REPLACE       = 3;
var ADDITIVE_MODE_MULTIPLY      = 4;
var ADDITIVE_MODE_NONE          = 5;

var aAddittiveModeInMap = {
    'base'          : ADDITIVE_MODE_BASE,
    'sum'           : ADDITIVE_MODE_SUM,
    'replace'       : ADDITIVE_MODE_REPLACE,
    'multiply'      : ADDITIVE_MODE_MULTIPLY,
    'none'          : ADDITIVE_MODE_NONE
};

var aAddittiveModeOutMap = [ 'unknown', 'base', 'sum', 'replace', 'multiply', 'none' ];


// Accumulate Modes
var ACCUMULATE_MODE_NONE        = 0;
var ACCUMULATE_MODE_SUM         = 1;

var aAccumulateModeOutMap = [ 'none', 'sum' ];

// Calculation Modes
var CALC_MODE_DISCRETE          = 1;
var CALC_MODE_LINEAR            = 2;
var CALC_MODE_PACED             = 3;
var CALC_MODE_SPLINE            = 4;

var aCalcModeInMap = {
    'discrete'      : CALC_MODE_DISCRETE,
    'linear'        : CALC_MODE_LINEAR,
    'paced'         : CALC_MODE_PACED,
    'spline'        : CALC_MODE_SPLINE
};

var aCalcModeOutMap = [ 'unknown', 'discrete', 'linear', 'paced', 'spline' ];


// Color Spaces
var COLOR_SPACE_RGB = 0;
var COLOR_SPACE_HSL = 1;

var aColorSpaceInMap = { 'rgb': COLOR_SPACE_RGB, 'hsl': COLOR_SPACE_HSL };

var aColorSpaceOutMap = [ 'rgb', 'hsl' ];


// Clock Directions
var CLOCKWISE               = 0;
var COUNTERCLOCKWISE        = 1;

var aClockDirectionInMap = { 'clockwise': CLOCKWISE, 'counter-clockwise': COUNTERCLOCKWISE };

var aClockDirectionOutMap = [ 'clockwise', 'counter-clockwise' ];


// Attribute Value Types
var UNKNOWN_PROPERTY        = 0; // eslint-disable-line no-unused-vars
var NUMBER_PROPERTY         = 1;
var ENUM_PROPERTY           = 2;
var COLOR_PROPERTY          = 3;
var STRING_PROPERTY         = 4;
var BOOL_PROPERTY           = 5;
var TUPLE_NUMBER_PROPERTY   = 6;

var aValueTypeOutMap = [ 'unknown', 'number', 'enum', 'color', 'string', 'boolean' ];


// Attribute Map
var aAttributeMap =
{
        'height':           {   'type':         NUMBER_PROPERTY,
                                'get':          'getHeight',
                                'set':          'setHeight',
                                'getmod':       'makeScaler( 1/nHeight )',
                                'setmod':       'makeScaler( nHeight)'          },

        'opacity':          {   'type':         NUMBER_PROPERTY,
                                'get':          'getOpacity',
                                'set':          'setOpacity'                    },

        'scale':           {   'type':          TUPLE_NUMBER_PROPERTY,
                                'get':          'getSize',
                                'set':          'setSize'                       },

        'translate':       {   'type':          TUPLE_NUMBER_PROPERTY,
                                'get':          'getPos',
                                'set':          'setPos'                        },

        'rotate':           {   'type':         NUMBER_PROPERTY,
                                'get':          'getRotationAngle',
                                'set':          'setRotationAngle'              },

        'width':            {   'type':         NUMBER_PROPERTY,
                                'get':          'getWidth',
                                'set':          'setWidth',
                                'getmod':       'makeScaler( 1/nWidth )',
                                'setmod':       'makeScaler( nWidth)'           },

        'x':                {   'type':         NUMBER_PROPERTY,
                                'get':          'getX',
                                'set':          'setX',
                                'getmod':       'makeScaler( 1/nWidth )',
                                'setmod':       'makeScaler( nWidth)'           },

        'y':                {   'type':         NUMBER_PROPERTY,
                                'get':          'getY',
                                'set':          'setY',
                                'getmod':       'makeScaler( 1/nHeight )',
                                'setmod':       'makeScaler( nHeight)'          },

        'fill':             {   'type':         ENUM_PROPERTY,
                                'get':          'getFillStyle',
                                'set':          'setFillStyle'                  },

        'stroke':           {   'type':         ENUM_PROPERTY,
                                'get':          'getStrokeStyle',
                                'set':          'setStrokeStyle'                },

        'visibility':       {   'type':         ENUM_PROPERTY,
                                'get':          'getVisibility',
                                'set':          'setVisibility'                 },

        'fill-color':       {   'type':         COLOR_PROPERTY,
                                'get':          'getFillColor',
                                'set':          'setFillColor'                  },

        'stroke-color':     {   'type':         COLOR_PROPERTY,
                                'get':          'getStrokeColor',
                                'set':          'setStrokeColor'                },

        'color':            {   'type':         COLOR_PROPERTY,
                                'get':          'getFontColor',
                                'set':          'setFontColor'                  }

};


// Transition Classes
var TRANSITION_INVALID              = 0;    // Invalid type
var TRANSITION_CLIP_POLYPOLYGON     = 1;    // Transition expressed by parametric clip polygon
var TRANSITION_SPECIAL              = 2;    // Transition expressed by hand-crafted function

/*
 * All Transition types should be in sync with aTransitionTypeInMap:
 * Comments '//' followed by integers represent the transition values in their
 * C++ implementations.
 */

// Transition Types
var BARWIPE_TRANSITION              = 1;
var BOXWIPE_TRANSITION              = 2;
var FOURBOXWIPE_TRANSITION          = 3;
var ELLIPSEWIPE_TRANSITION          = 4; // 17
var CLOCKWIPE_TRANSITION            = 5; // 22
var PINWHEELWIPE_TRANSITION         = 6; // 23
var PUSHWIPE_TRANSITION             = 7; // 35
var SLIDEWIPE_TRANSITION            = 8; // 36
var FADE_TRANSITION                 = 9; // 37
var RANDOMBARWIPE_TRANSITION        = 10; // 38
var CHECKERBOARDWIPE_TRANSITION     = 11; // 39
var DISSOLVE_TRANSITION             = 12; // 40
var SNAKEWIPE_TRANSITION            = 13; // 30
var PARALLELSNAKESWIPE_TRANSITION   = 14; // 32
var IRISWIPE_TRANSITION             = 15; // 12
var BARNDOORWIPE_TRANSITION         = 16; // 4
var VEEWIPE_TRANSITION              = 17; // 8
var ZIGZAGWIPE_TRANSITION           = 18; // 10
var BARNZIGZAGWIPE_TRANSITION       = 19; // 11
var FANWIPE_TRANSITION              = 20; // 25
var SINGLESWEEPWIPE_TRANSITION      = 21; // 24
var WATERFALLWIPE_TRANSITION        = 22; // 34
var SPIRALWIPE_TRANSITION           = 23; // 31
var MISCDIAGONALWIPE_TRANSITION     = 24; // 7
var BOXSNAKESWIPE_TRANSITION        = 25; // 33

var aTransitionTypeInMap = {
    'barWipe'           : BARWIPE_TRANSITION,
    'boxWipe'           : BOXWIPE_TRANSITION,
    'barnDoorWipe'      : BARNDOORWIPE_TRANSITION,
    'fourBoxWipe'       : FOURBOXWIPE_TRANSITION,
    'ellipseWipe'       : ELLIPSEWIPE_TRANSITION,
    'clockWipe'         : CLOCKWIPE_TRANSITION,
    'pinWheelWipe'      : PINWHEELWIPE_TRANSITION,
    'miscDiagonalWipe'  : MISCDIAGONALWIPE_TRANSITION,
    'pushWipe'          : PUSHWIPE_TRANSITION,
    'slideWipe'         : SLIDEWIPE_TRANSITION,
    'fade'              : FADE_TRANSITION,
    'fanWipe'           : FANWIPE_TRANSITION,
    'randomBarWipe'     : RANDOMBARWIPE_TRANSITION,
    'checkerBoardWipe'  : CHECKERBOARDWIPE_TRANSITION,
    'dissolve'          : DISSOLVE_TRANSITION,
    'singleSweepWipe'   : SINGLESWEEPWIPE_TRANSITION,
    'snakeWipe'         : SNAKEWIPE_TRANSITION,
    'parallelSnakesWipe': PARALLELSNAKESWIPE_TRANSITION,
    'spiralWipe'        : SPIRALWIPE_TRANSITION,
    'boxSnakesWipe'     : BOXSNAKESWIPE_TRANSITION,
    'irisWipe'          : IRISWIPE_TRANSITION,
    'veeWipe'           : VEEWIPE_TRANSITION,
    'zigZagWipe'        : ZIGZAGWIPE_TRANSITION,
    'barnZigZagWipe'    : BARNZIGZAGWIPE_TRANSITION,
    'waterfallWipe'     : WATERFALLWIPE_TRANSITION
};

/*
 * All Transition subtypes should be in sync with aTransitionSubtypeInMap:
 * Comments '//' followed by integers represent the transition values in their
 * C++ implementations.
 */
// Transition Subtypes
var DEFAULT_TRANS_SUBTYPE                       = 0;
var LEFTTORIGHT_TRANS_SUBTYPE                   = 1;
var TOPTOBOTTOM_TRANS_SUBTYPE                   = 2;
var CORNERSIN_TRANS_SUBTYPE                     = 3; // 11
var CORNERSOUT_TRANS_SUBTYPE                    = 4;
var VERTICAL_TRANS_SUBTYPE                      = 5;
var HORIZONTAL_TRANS_SUBTYPE                    = 6; // 14
var DOWN_TRANS_SUBTYPE                          = 7; // 19
var CIRCLE_TRANS_SUBTYPE                        = 8; // 27
var CLOCKWISETWELVE_TRANS_SUBTYPE               = 9; // 33
var CLOCKWISETHREE_TRANS_SUBTYPE                = 10;
var CLOCKWISESIX_TRANS_SUBTYPE                  = 11;
var CLOCKWISENINE_TRANS_SUBTYPE                 = 12;
var TWOBLADEVERTICAL_TRANS_SUBTYPE              = 13;
var TWOBLADEHORIZONTAL_TRANS_SUBTYPE            = 14;
var FOURBLADE_TRANS_SUBTYPE                     = 15; // 39
var FROMLEFT_TRANS_SUBTYPE                      = 16; // 97
var FROMTOP_TRANS_SUBTYPE                       = 17;
var FROMRIGHT_TRANS_SUBTYPE                     = 18;
var FROMBOTTOM_TRANS_SUBTYPE                    = 19;
var CROSSFADE_TRANS_SUBTYPE                     = 20;
var FADETOCOLOR_TRANS_SUBTYPE                   = 21;
var FADEFROMCOLOR_TRANS_SUBTYPE                 = 22;
var FADEOVERCOLOR_TRANS_SUBTYPE                 = 23;
var THREEBLADE_TRANS_SUBTYPE                    = 24;
var EIGHTBLADE_TRANS_SUBTYPE                    = 25;
var ONEBLADE_TRANS_SUBTYPE                      = 26; // 107
var ACROSS_TRANS_SUBTYPE                        = 27;
var TOPLEFTVERTICAL_TRANS_SUBTYPE               = 28; // 109
var TOPLEFTHORIZONTAL_TRANS_SUBTYPE             = 29; // 64
var TOPLEFTDIAGONAL_TRANS_SUBTYPE               = 30; // 65
var TOPRIGHTDIAGONAL_TRANS_SUBTYPE              = 31; // 66
var BOTTOMRIGHTDIAGONAL_TRANS_SUBTYPE           = 32; // 67
var BOTTOMLEFTDIAGONAL_TRANS_SUBTYPE            = 33; // 68
var RECTANGLE_TRANS_SUBTYPE                     = 34; // 101
var DIAMOND_TRANS_SUBTYPE                       = 35; // 102
var TOPLEFT_TRANS_SUBTYPE                       = 36; // 3
var TOPRIGHT_TRANS_SUBTYPE                      = 37; // 4
var BOTTOMRIGHT_TRANS_SUBTYPE                   = 38; // 5
var BOTTOMLEFT_TRANS_SUBTYPE                    = 39; // 6
var TOPCENTER_TRANS_SUBTYPE                     = 40; // 7
var RIGHTCENTER_TRANS_SUBTYPE                   = 41; // 8
var BOTTOMCENTER_TRANS_SUBTYPE                  = 42; // 9
var LEFTCENTER_TRANS_SUBTYPE                    = 43; // 10
var LEFT_TRANS_SUBTYPE                          = 44; // 20
var UP_TRANS_SUBTYPE                            = 45; // 21
var RIGHT_TRANS_SUBTYPE                         = 46; // 22
var DIAGONALBOTTOMLEFT_TRANS_SUBTYPE            = 47; // 15
var DIAGONALTOPLEFT_TRANS_SUBTYPE               = 48; // 16
var CENTERTOP_TRANS_SUBTYPE                     = 49; // 48
var CENTERRIGHT_TRANS_SUBTYPE                   = 50; // 49
var TOP_TRANS_SUBTYPE                           = 51; // 50
var BOTTOM_TRANS_SUBTYPE                        = 52; // 52
var CLOCKWISETOP_TRANS_SUBTYPE                  = 53; // 40
var CLOCKWISERIGHT_TRANS_SUBTYPE                = 54; // 41
var CLOCKWISEBOTTOM_TRANS_SUBTYPE               = 55; // 42
var CLOCKWISELEFT_TRANS_SUBTYPE                 = 56; // 43
var CLOCKWISETOPLEFT_TRANS_SUBTYPE              = 57; // 44
var COUNTERCLOCKWISEBOTTOMLEFT_TRANS_SUBTYPE    = 58; // 45
var CLOCKWISEBOTTOMRIGHT_TRANS_SUBTYPE          = 59; // 46
var COUNTERCLOCKWISETOPRIGHT_TRANS_SUBTYPE      = 60; // 47
var VERTICALLEFT_TRANS_SUBTYPE                  = 61; // 93
var VERTICALRIGHT_TRANS_SUBTYPE                 = 62; // 94
var HORIZONTALLEFT_TRANS_SUBTYPE                = 63; // 95
var HORIZONTALRIGHT_TRANS_SUBTYPE               = 64; // 96
var TOPLEFTCLOCKWISE_TRANS_SUBTYPE              = 65; // 69
var TOPRIGHTCLOCKWISE_TRANS_SUBTYPE             = 66; // 70
var BOTTOMRIGHTCLOCKWISE_TRANS_SUBTYPE          = 67; // 71
var BOTTOMLEFTCLOCKWISE_TRANS_SUBTYPE           = 68; // 72
var TOPLEFTCOUNTERCLOCKWISE_TRANS_SUBTYPE       = 69; // 73
var TOPRIGHTCOUNTERCLOCKWISE_TRANS_SUBTYPE      = 70; // 74
var BOTTOMRIGHTCOUNTERCLOCKWISE_TRANS_SUBTYPE   = 71; // 75
var BOTTOMLEFTCOUNTERCLOCKWISE_TRANS_SUBTYPE    = 72; // 76
var DOUBLEBARNDOOR_TRANS_SUBTYPE                = 73; // 17
var DOUBLEDIAMOND_TRANS_SUBTYPE                 = 74; // 18
var VERTICALTOPSAME_TRANS_SUBTYPE               = 75; // 77
var VERTICALBOTTOMSAME_TRANS_SUBTYPE            = 76; // 78
var VERTICALTOPLEFTOPPOSITE_TRANS_SUBTYPE       = 77; // 79
var VERTICALBOTTOMLEFTOPPOSITE_TRANS_SUBTYPE    = 78; // 80
var HORIZONTALLEFTSAME_TRANS_SUBTYPE            = 79; // 81
var HORIZONTALRIGHTSAME_TRANS_SUBTYPE           = 80; // 82
var HORIZONTALTOPLEFTOPPOSITE_TRANS_SUBTYPE     = 81; // 83
var HORIZONTALTOPRIGHTOPPOSITE_TRANS_SUBTYPE    = 82; // 84
var DIAGONALBOTTOMLEFTOPPOSITE_TRANS_SUBTYPE    = 83; // 85
var DIAGONALTOPLEFTOPPOSITE_TRANS_SUBTYPE       = 84; // 86
var TWOBOXTOP_TRANS_SUBTYPE                     = 85; // 87
var TWOBOXBOTTOM_TRANS_SUBTYPE                  = 86; // 88
var TWOBOXLEFT_TRANS_SUBTYPE                    = 87; // 89
var TWOBOXRIGHT_TRANS_SUBTYPE                   = 88; // 90
var FOURBOXVERTICAL_TRANS_SUBTYPE               = 89; // 91
var FOURBOXHORIZONTAL_TRANS_SUBTYPE             = 90; // 92

var aTransitionSubtypeInMap = {
    'default'                       : DEFAULT_TRANS_SUBTYPE,
    'leftToRight'                   : LEFTTORIGHT_TRANS_SUBTYPE,
    'topToBottom'                   : TOPTOBOTTOM_TRANS_SUBTYPE,
    'cornersIn'                     : CORNERSIN_TRANS_SUBTYPE,
    'cornersOut'                    : CORNERSOUT_TRANS_SUBTYPE,
    'vertical'                      : VERTICAL_TRANS_SUBTYPE,
    'centerTop'                     : CENTERTOP_TRANS_SUBTYPE,
    'centerRight'                   : CENTERRIGHT_TRANS_SUBTYPE,
    'top'                           : TOP_TRANS_SUBTYPE,
    'right'                         : RIGHT_TRANS_SUBTYPE,
    'bottom'                        : BOTTOM_TRANS_SUBTYPE,
    'left'                          : LEFT_TRANS_SUBTYPE,
    'horizontal'                    : HORIZONTAL_TRANS_SUBTYPE,
    'down'                          : DOWN_TRANS_SUBTYPE,
    'circle'                        : CIRCLE_TRANS_SUBTYPE,
    'clockwiseTwelve'               : CLOCKWISETWELVE_TRANS_SUBTYPE,
    'clockwiseThree'                : CLOCKWISETHREE_TRANS_SUBTYPE,
    'clockwiseSix'                  : CLOCKWISESIX_TRANS_SUBTYPE,
    'clockwiseNine'                 : CLOCKWISENINE_TRANS_SUBTYPE,
    'clockwiseRight'                : CLOCKWISERIGHT_TRANS_SUBTYPE,
    'clockwiseTop'                  : CLOCKWISETOP_TRANS_SUBTYPE,
    'clockwiseBottom'               : CLOCKWISEBOTTOM_TRANS_SUBTYPE,
    'clockwiseLeft'                 : CLOCKWISELEFT_TRANS_SUBTYPE,
    'clockwiseTopLeft'              : CLOCKWISETOPLEFT_TRANS_SUBTYPE,
    'counterClockwiseBottomLeft'    : COUNTERCLOCKWISEBOTTOMLEFT_TRANS_SUBTYPE,
    'clockwiseBottomRight'          : CLOCKWISEBOTTOMRIGHT_TRANS_SUBTYPE,
    'counterClockwiseTopRight'      : COUNTERCLOCKWISETOPRIGHT_TRANS_SUBTYPE,
    'twoBladeVertical'              : TWOBLADEVERTICAL_TRANS_SUBTYPE,
    'twoBladeHorizontal'            : TWOBLADEHORIZONTAL_TRANS_SUBTYPE,
    'fourBlade'                     : FOURBLADE_TRANS_SUBTYPE,
    'fromLeft'                      : FROMLEFT_TRANS_SUBTYPE,
    'fromTop'                       : FROMTOP_TRANS_SUBTYPE,
    'fromRight'                     : FROMRIGHT_TRANS_SUBTYPE,
    'fromBottom'                    : FROMBOTTOM_TRANS_SUBTYPE,
    'crossfade'                     : CROSSFADE_TRANS_SUBTYPE,
    'fadeToColor'                   : FADETOCOLOR_TRANS_SUBTYPE,
    'fadeFromColor'                 : FADEFROMCOLOR_TRANS_SUBTYPE,
    'fadeOverColor'                 : FADEOVERCOLOR_TRANS_SUBTYPE,
    'threeBlade'                    : THREEBLADE_TRANS_SUBTYPE,
    'eightBlade'                    : EIGHTBLADE_TRANS_SUBTYPE,
    'oneBlade'                      : ONEBLADE_TRANS_SUBTYPE,
    'across'                        : ACROSS_TRANS_SUBTYPE,
    'topLeftVertical'               : TOPLEFTVERTICAL_TRANS_SUBTYPE,
    'topLeftHorizontal'             : TOPLEFTHORIZONTAL_TRANS_SUBTYPE,
    'topLeftDiagonal'               : TOPLEFTDIAGONAL_TRANS_SUBTYPE,
    'topRightDiagonal'              : TOPRIGHTDIAGONAL_TRANS_SUBTYPE,
    'bottomRightDiagonal'           : BOTTOMRIGHTDIAGONAL_TRANS_SUBTYPE,
    'topLeftClockwise'              : TOPLEFTCLOCKWISE_TRANS_SUBTYPE,
    'topRightClockwise'             : TOPRIGHTCLOCKWISE_TRANS_SUBTYPE,
    'bottomRightClockwise'          : BOTTOMRIGHTCLOCKWISE_TRANS_SUBTYPE,
    'bottomLeftClockwise'           : BOTTOMLEFTCLOCKWISE_TRANS_SUBTYPE,
    'topLeftCounterClockwise'       : TOPLEFTCOUNTERCLOCKWISE_TRANS_SUBTYPE,
    'topRightCounterClockwise'      : TOPRIGHTCOUNTERCLOCKWISE_TRANS_SUBTYPE,
    'bottomRightCounterClockwise'   : BOTTOMRIGHTCOUNTERCLOCKWISE_TRANS_SUBTYPE,
    'bottomLeftCounterClockwise'    : BOTTOMLEFTCOUNTERCLOCKWISE_TRANS_SUBTYPE,
    'bottomLeftDiagonal'            : BOTTOMLEFTDIAGONAL_TRANS_SUBTYPE,
    'rectangle'                     : RECTANGLE_TRANS_SUBTYPE,
    'diamond'                       : DIAMOND_TRANS_SUBTYPE,
    'topLeft'                       : TOPLEFT_TRANS_SUBTYPE,
    'topRight'                      : TOPRIGHT_TRANS_SUBTYPE,
    'bottomRight'                   : BOTTOMRIGHT_TRANS_SUBTYPE,
    'bottomLeft'                    : BOTTOMLEFT_TRANS_SUBTYPE,
    'topCenter'                     : TOPCENTER_TRANS_SUBTYPE,
    'rightCenter'                   : RIGHTCENTER_TRANS_SUBTYPE,
    'bottomCenter'                  : BOTTOMCENTER_TRANS_SUBTYPE,
    'leftCenter'                    : LEFTCENTER_TRANS_SUBTYPE,
    'up'                            : UP_TRANS_SUBTYPE,
    'diagonalBottomLeft'            : DIAGONALBOTTOMLEFT_TRANS_SUBTYPE,
    'diagonalTopLeft'               : DIAGONALTOPLEFT_TRANS_SUBTYPE,
    'verticalLeft'                  : VERTICALLEFT_TRANS_SUBTYPE,
    'verticalRight'                 : VERTICALRIGHT_TRANS_SUBTYPE,
    'horizontalLeft'                : HORIZONTALLEFT_TRANS_SUBTYPE,
    'horizontalRight'               : HORIZONTALRIGHT_TRANS_SUBTYPE,
    'doubleBarnDoor'                : DOUBLEBARNDOOR_TRANS_SUBTYPE,
    'doubleDiamond'                 : DOUBLEDIAMOND_TRANS_SUBTYPE,
    'verticalTopSame'               : VERTICALTOPSAME_TRANS_SUBTYPE,
    'verticalBottomSame'            : VERTICALBOTTOMSAME_TRANS_SUBTYPE,
    'verticalTopLeftOpposite'       : VERTICALTOPLEFTOPPOSITE_TRANS_SUBTYPE,
    'verticalBottomLeftOpposite'    : VERTICALBOTTOMLEFTOPPOSITE_TRANS_SUBTYPE,
    'horizontalLeftSame'            : HORIZONTALLEFTSAME_TRANS_SUBTYPE,
    'horizontalRightSame'           : HORIZONTALRIGHTSAME_TRANS_SUBTYPE,
    'horizontalTopLeftOpposite'     : HORIZONTALTOPLEFTOPPOSITE_TRANS_SUBTYPE,
    'horizontalTopRightOpposite'    : HORIZONTALTOPRIGHTOPPOSITE_TRANS_SUBTYPE,
    'diagonalBottomLeftOpposite'    : DIAGONALBOTTOMLEFTOPPOSITE_TRANS_SUBTYPE,
    'diagonalTopLeftOpposite'       : DIAGONALTOPLEFTOPPOSITE_TRANS_SUBTYPE,
    'twoBoxTop'                     : TWOBOXTOP_TRANS_SUBTYPE,
    'twoBoxBottom'                  : TWOBOXBOTTOM_TRANS_SUBTYPE,
    'twoBoxLeft'                    : TWOBOXLEFT_TRANS_SUBTYPE,
    'twoBoxRight'                   : TWOBOXRIGHT_TRANS_SUBTYPE,
    'fourBoxVertical'               : FOURBOXVERTICAL_TRANS_SUBTYPE,
    'fourBoxHorizontal'             : FOURBOXHORIZONTAL_TRANS_SUBTYPE
};

// Transition Modes
var TRANSITION_MODE_IN  = 1;
var TRANSITION_MODE_OUT = 0;

var aTransitionModeOutMap = [ 'out', 'in' ];


// Transition Reverse Methods

// Ignore direction attribute altogether.
// (If it has no sensible meaning for this transition.)
var REVERSEMETHOD_IGNORE                    = 0;
// Revert by changing the direction of the parameter sweep.
// (From 1->0 instead of 0->1)
var REVERSEMETHOD_INVERT_SWEEP              = 1;
// Revert by subtracting the generated polygon from the target bound rect.
var REVERSEMETHOD_SUBTRACT_POLYGON          = 2;
// Combination of REVERSEMETHOD_INVERT_SWEEP and REVERSEMETHOD_SUBTRACT_POLYGON.
var REVERSEMETHOD_SUBTRACT_AND_INVERT       = 3;
// Reverse by rotating polygon 180 degrees.
var REVERSEMETHOD_ROTATE_180                = 4;
// Reverse by flipping polygon at the y axis.
var REVERSEMETHOD_FLIP_X                    = 5;
// Reverse by flipping polygon at the x axis.
var REVERSEMETHOD_FLIP_Y                    = 6;

// eslint-disable-next-line no-unused-vars
var aReverseMethodOutMap = ['ignore', 'invert sweep', 'subtract polygon',
                            'subtract and invert', 'rotate 180', 'flip x', 'flip y'];


// Transition filter info table

var aTransitionInfoTable = {};

// type: fake transition
aTransitionInfoTable[0] = {};
// subtype: default
aTransitionInfoTable[0][0] =
{
    'class' : TRANSITION_INVALID,
    'rotationAngle' : 0.0,
    'scaleX' : 0.0,
    'scaleY' : 0.0,
    'reverseMethod' : REVERSEMETHOD_IGNORE,
    'outInvertsSweep' : false,
    'scaleIsotropically' : false
};

aTransitionInfoTable[SNAKEWIPE_TRANSITION] = {};
aTransitionInfoTable[SNAKEWIPE_TRANSITION][TOPLEFTVERTICAL_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : -90.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_ROTATE_180,
    'outInvertsSweep' : true,
    'scaleIsotropically' : false
};
aTransitionInfoTable[SNAKEWIPE_TRANSITION][TOPLEFTHORIZONTAL_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 0.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_ROTATE_180,
    'outInvertSweep' : true,
    'scaleIsotropically' : false
};
aTransitionInfoTable[SNAKEWIPE_TRANSITION][TOPLEFTDIAGONAL_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 0.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_ROTATE_180,
    'outInvertSweep' : true,
    'scaleIsotropically' : false
};
aTransitionInfoTable[SNAKEWIPE_TRANSITION][TOPRIGHTDIAGONAL_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 0.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_ROTATE_180,
    'outInvertSweep' : true,
    'scaleIsotropically' : false
};
aTransitionInfoTable[SNAKEWIPE_TRANSITION][BOTTOMRIGHTDIAGONAL_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 180.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_ROTATE_180,
    'outInvertSweep' : true,
    'scaleIsotropically' : false
};
aTransitionInfoTable[SNAKEWIPE_TRANSITION][BOTTOMLEFTDIAGONAL_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 180.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_ROTATE_180,
    'outInvertSweep' : true,
    'scaleIsotropically' : false
}

aTransitionInfoTable[PARALLELSNAKESWIPE_TRANSITION] = {};
aTransitionInfoTable[PARALLELSNAKESWIPE_TRANSITION][VERTICALTOPSAME_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 0.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_IGNORE,
    'outInvertSweep' : true,
    'scaleIsotropically' : false
};
aTransitionInfoTable[PARALLELSNAKESWIPE_TRANSITION][VERTICALBOTTOMSAME_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 180.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_IGNORE,
    'outInvertSweep' : true,
    'scaleIsotropically' : false
};
aTransitionInfoTable[PARALLELSNAKESWIPE_TRANSITION][VERTICALTOPLEFTOPPOSITE_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 0.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_IGNORE,
    'outInvertSweep' : true,
    'scaleIsotropically' : false
};
aTransitionInfoTable[PARALLELSNAKESWIPE_TRANSITION][VERTICALBOTTOMLEFTOPPOSITE_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 0.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_IGNORE,
    'outInvertSweep' : true,
    'scaleIsotropically' : false
};
aTransitionInfoTable[PARALLELSNAKESWIPE_TRANSITION][HORIZONTALLEFTSAME_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : -90.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_IGNORE,
    'outInvertSweep' : true,
    'scaleIsotropically' : false
};
aTransitionInfoTable[PARALLELSNAKESWIPE_TRANSITION][HORIZONTALRIGHTSAME_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 90.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_IGNORE,
    'outInvertSweep' : true,
    'scaleIsotropically' : false
};
aTransitionInfoTable[PARALLELSNAKESWIPE_TRANSITION][HORIZONTALTOPLEFTOPPOSITE_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : -90.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_IGNORE,
    'outInvertSweep' : true,
    'scaleIsotropically' : false
};
aTransitionInfoTable[PARALLELSNAKESWIPE_TRANSITION][HORIZONTALTOPRIGHTOPPOSITE_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : -90.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_IGNORE,
    'outInvertSweep' : true,
    'scaleIsotropically' : false
};
aTransitionInfoTable[PARALLELSNAKESWIPE_TRANSITION][DIAGONALTOPLEFTOPPOSITE_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 0.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_IGNORE,
    'outInvertSweep' : true,
    'scaleIsotropically' : false
};
aTransitionInfoTable[PARALLELSNAKESWIPE_TRANSITION][DIAGONALBOTTOMLEFTOPPOSITE_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 0.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_IGNORE,
    'outInvertSweep' : true,
    'scaleIsotropically' : false
};

aTransitionInfoTable[SPIRALWIPE_TRANSITION] = {};
aTransitionInfoTable[SPIRALWIPE_TRANSITION][TOPLEFTCLOCKWISE_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 0.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_SUBTRACT_AND_INVERT,
    'outInvertSweep' : true,
    'scaleIsotropically' : false
};
aTransitionInfoTable[SPIRALWIPE_TRANSITION][TOPRIGHTCLOCKWISE_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 90.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_SUBTRACT_AND_INVERT,
    'outInvertSweep' : true,
    'scaleIsotropically' : false
};
aTransitionInfoTable[SPIRALWIPE_TRANSITION][BOTTOMRIGHTCLOCKWISE_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 180.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_SUBTRACT_AND_INVERT,
    'outInvertSweep' : true,
    'scaleIsotropically' : false
};
aTransitionInfoTable[SPIRALWIPE_TRANSITION][BOTTOMLEFTCLOCKWISE_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 270.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_SUBTRACT_AND_INVERT,
    'outInvertSweep' : true,
    'scaleIsotropically' : false
};
aTransitionInfoTable[SPIRALWIPE_TRANSITION][TOPLEFTCOUNTERCLOCKWISE_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 90.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_SUBTRACT_AND_INVERT,
    'outInvertSweep' : true,
    'scaleIsotropically' : false
};
aTransitionInfoTable[SPIRALWIPE_TRANSITION][TOPRIGHTCOUNTERCLOCKWISE_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 180.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_SUBTRACT_AND_INVERT,
    'outInvertSweep' : true,
    'scaleIsotropically' : false
};
aTransitionInfoTable[SPIRALWIPE_TRANSITION][BOTTOMRIGHTCOUNTERCLOCKWISE_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 270.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_SUBTRACT_AND_INVERT,
    'outInvertSweep' : true,
    'scaleIsotropically' : false
};
aTransitionInfoTable[SPIRALWIPE_TRANSITION][BOTTOMLEFTCOUNTERCLOCKWISE_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 0.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_SUBTRACT_AND_INVERT,
    'outInvertSweep' : true,
    'scaleIsotropically' : false
};

aTransitionInfoTable[BOXSNAKESWIPE_TRANSITION] = {};
aTransitionInfoTable[BOXSNAKESWIPE_TRANSITION][TWOBOXTOP_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 90.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_IGNORE,
    'outInvertSweep' : true,
    'scaleIsotropically' : false
};
aTransitionInfoTable[BOXSNAKESWIPE_TRANSITION][TWOBOXBOTTOM_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : -90.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_IGNORE,
    'outInvertSweep' : true,
    'scaleIsotropically' : false
};
aTransitionInfoTable[BOXSNAKESWIPE_TRANSITION][TWOBOXLEFT_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 0.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_IGNORE,
    'outInvertSweep' : true,
    'scaleIsotropically' : false
};
aTransitionInfoTable[BOXSNAKESWIPE_TRANSITION][TWOBOXRIGHT_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 180.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_IGNORE,
    'outInvertSweep' : true,
    'scaleIsotropically' : false
};
aTransitionInfoTable[BOXSNAKESWIPE_TRANSITION][FOURBOXVERTICAL_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 90.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_IGNORE,
    'outInvertSweep' : true,
    'scaleIsotropically' : false
};
aTransitionInfoTable[BOXSNAKESWIPE_TRANSITION][FOURBOXHORIZONTAL_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 0.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_IGNORE,
    'outInvertSweep' : true,
    'scaleIsotropically' : false
};

aTransitionInfoTable[BARNDOORWIPE_TRANSITION] = {};
aTransitionInfoTable[BARNDOORWIPE_TRANSITION][VERTICAL_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle': 0.0,
    'scaleX': 1.0,
    'scaleY': 1.0,
    'reverseMethod': REVERSEMETHOD_SUBTRACT_AND_INVERT,
    'outInvertsSweep': true,
    'scaleIsotropically': false
};
aTransitionInfoTable[BARNDOORWIPE_TRANSITION][HORIZONTAL_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle': 90.0,
    'scaleX': 1.0,
    'scaleY': 1.0,
    'reverseMethod': REVERSEMETHOD_SUBTRACT_AND_INVERT,
    'outInvertsSweep': true,
    'scaleIsotropically': false
};
aTransitionInfoTable[BARNDOORWIPE_TRANSITION][DIAGONALBOTTOMLEFT_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle': 45.0,
    'scaleX': Math.SQRT2,
    'scaleY': Math.SQRT2,
    'reverseMethod': REVERSEMETHOD_SUBTRACT_AND_INVERT,
    'outInvertsSweep': true,
    'scaleIsotropically': false
};
aTransitionInfoTable[BARNDOORWIPE_TRANSITION][DIAGONALTOPLEFT_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle': -45.0,
    'scaleX': Math.SQRT2,
    'scaleY': Math.SQRT2,
    'reverseMethod': REVERSEMETHOD_SUBTRACT_AND_INVERT,
    'outInvertsSweep': true,
    'scaleIsotropically': false
};

aTransitionInfoTable[MISCDIAGONALWIPE_TRANSITION] = {};
aTransitionInfoTable[MISCDIAGONALWIPE_TRANSITION][DOUBLEBARNDOOR_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle': 45.0,
    'scaleX': Math.SQRT2,
    'scaleY': Math.SQRT2,
    'reverseMethod': REVERSEMETHOD_IGNORE,
    'outInvertsSweep': true,
    'scaleIsotropically': false
};
aTransitionInfoTable[MISCDIAGONALWIPE_TRANSITION][DOUBLEDIAMOND_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle': 0.0,
    'scaleX': 1,
    'scaleY': 1,
    'reverseMethod': REVERSEMETHOD_IGNORE,
    'outInvertsSweep': true,
    'scaleIsotropically': false
};

aTransitionInfoTable[IRISWIPE_TRANSITION] = {};
aTransitionInfoTable[IRISWIPE_TRANSITION][RECTANGLE_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle': 0.0,
    'scaleX': 1.0,
    'scaleY': 1.0,
    'reverseMethod': REVERSEMETHOD_SUBTRACT_AND_INVERT,
    'outInvertsSweep': true,
    'scaleIsotropically': false
};

aTransitionInfoTable[IRISWIPE_TRANSITION][DIAMOND_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle': 45.0,
    'scaleX': Math.SQRT2,
    'scaleY': Math.SQRT2,
    'reverseMethod': REVERSEMETHOD_SUBTRACT_AND_INVERT,
    'outInvertsSweep': true,
    'scaleIsotropically': false
};

aTransitionInfoTable[ZIGZAGWIPE_TRANSITION] = {};
aTransitionInfoTable[ZIGZAGWIPE_TRANSITION][LEFTTORIGHT_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 0.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_FLIP_X,
    'outInvertsSweep' : true,
    'scaleIsotropically' : false
};
aTransitionInfoTable[ZIGZAGWIPE_TRANSITION][TOPTOBOTTOM_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 90.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_FLIP_Y,
    'outInvertsSweep' : true,
    'scaleIsotropically' : false
};

aTransitionInfoTable[BARNZIGZAGWIPE_TRANSITION] = {};
aTransitionInfoTable[BARNZIGZAGWIPE_TRANSITION][VERTICAL_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 0.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_IGNORE,
    'outInvertsSweep' : true,
    'scaleIsotropically' : false
};
aTransitionInfoTable[BARNZIGZAGWIPE_TRANSITION][HORIZONTAL_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 90.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_IGNORE,
    'outInvertsSweep' : true,
    'scaleIsotropically' : false
};

aTransitionInfoTable[BARWIPE_TRANSITION] = {};
aTransitionInfoTable[BARWIPE_TRANSITION][LEFTTORIGHT_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 0.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_FLIP_X,
    'outInvertsSweep' : false,
    'scaleIsotropically' : false
};
aTransitionInfoTable[BARWIPE_TRANSITION][TOPTOBOTTOM_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 90.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_FLIP_Y,
    'outInvertsSweep' : false,
    'scaleIsotropically' : false
};

aTransitionInfoTable[WATERFALLWIPE_TRANSITION] = {};
aTransitionInfoTable[WATERFALLWIPE_TRANSITION][VERTICALLEFT_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 0.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_ROTATE_180,
    'outInvertsSweep' : true,
    'scaleIsotropically' : false
};
aTransitionInfoTable[WATERFALLWIPE_TRANSITION][VERTICALRIGHT_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 0.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_ROTATE_180,
    'outInvertsSweep' : true,
    'scaleIsotropically' : false
};
aTransitionInfoTable[WATERFALLWIPE_TRANSITION][HORIZONTALLEFT_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : -90.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_ROTATE_180,
    'outInvertsSweep' : true,
    'scaleIsotropically' : false
};
aTransitionInfoTable[WATERFALLWIPE_TRANSITION][HORIZONTALRIGHT_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 90.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_ROTATE_180,
    'outInvertsSweep' : true,
    'scaleIsotropically' : false
};

aTransitionInfoTable[BOXWIPE_TRANSITION] = {};
aTransitionInfoTable[BOXWIPE_TRANSITION][TOPLEFT_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 0.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_IGNORE,
    'outInvertsSweep' : true,
    'scaleIsotropically' : false
};
aTransitionInfoTable[BOXWIPE_TRANSITION][TOPRIGHT_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 90.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_IGNORE,
    'outInvertsSweep' : true,
    'scaleIsotropically' : false
};
aTransitionInfoTable[BOXWIPE_TRANSITION][BOTTOMRIGHT_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 180.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_IGNORE,
    'outInvertsSweep' : true,
    'scaleIsotropically' : false
};
aTransitionInfoTable[BOXWIPE_TRANSITION][BOTTOMLEFT_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : -90.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_IGNORE,
    'outInvertsSweep' : true,
    'scaleIsotropically' : false
};
aTransitionInfoTable[BOXWIPE_TRANSITION][TOPCENTER_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 0.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_FLIP_Y,
    'outInvertsSweep' : true,
    'scaleIsotropically' : false
};
aTransitionInfoTable[BOXWIPE_TRANSITION][RIGHTCENTER_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 90.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_FLIP_X,
    'outInvertsSweep' : true,
    'scaleIsotropically' : false
};
aTransitionInfoTable[BOXWIPE_TRANSITION][BOTTOMCENTER_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 180.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_FLIP_Y,
    'outInvertsSweep' : true,
    'scaleIsotropically' : false
};
aTransitionInfoTable[BOXWIPE_TRANSITION][LEFTCENTER_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : -90.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_FLIP_X,
    'outInvertsSweep' : true,
    'scaleIsotropically' : false
};

aTransitionInfoTable[FOURBOXWIPE_TRANSITION] = {};
aTransitionInfoTable[FOURBOXWIPE_TRANSITION][CORNERSIN_TRANS_SUBTYPE] =
aTransitionInfoTable[FOURBOXWIPE_TRANSITION][CORNERSOUT_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 0.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_SUBTRACT_AND_INVERT,
    'outInvertsSweep' : true,
    'scaleIsotropically' : false
};

aTransitionInfoTable[ELLIPSEWIPE_TRANSITION] = {};
aTransitionInfoTable[ELLIPSEWIPE_TRANSITION][CIRCLE_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 0.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_SUBTRACT_AND_INVERT,
    'outInvertsSweep' : true,
    'scaleIsotropically' : true
};
aTransitionInfoTable[ELLIPSEWIPE_TRANSITION][HORIZONTAL_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 0.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_SUBTRACT_AND_INVERT,
    'outInvertsSweep' : true,
    'scaleIsotropically' : false
};
aTransitionInfoTable[ELLIPSEWIPE_TRANSITION][VERTICAL_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 90.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_SUBTRACT_AND_INVERT,
    'outInvertsSweep' : true,
    'scaleIsotropically' : false
};

aTransitionInfoTable[CLOCKWIPE_TRANSITION] = {};
aTransitionInfoTable[CLOCKWIPE_TRANSITION][CLOCKWISETWELVE_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 0.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_FLIP_X,
    'outInvertsSweep' : true,
    'scaleIsotropically' : false
};
aTransitionInfoTable[CLOCKWIPE_TRANSITION][CLOCKWISETHREE_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 90.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_FLIP_Y,
    'outInvertsSweep' : true,
    'scaleIsotropically' : false
};
aTransitionInfoTable[CLOCKWIPE_TRANSITION][CLOCKWISESIX_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 180.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_FLIP_X,
    'outInvertsSweep' : true,
    'scaleIsotropically' : false
};
aTransitionInfoTable[CLOCKWIPE_TRANSITION][CLOCKWISENINE_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 270.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_FLIP_Y,
    'outInvertsSweep' : true,
    'scaleIsotropically' : false
};

aTransitionInfoTable[VEEWIPE_TRANSITION] = {};
aTransitionInfoTable[VEEWIPE_TRANSITION][DOWN_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 0.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_FLIP_Y,
    'outInvertsSweep' : true,
    'scaleIsotropically' : false
};
aTransitionInfoTable[VEEWIPE_TRANSITION][LEFT_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 90.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_FLIP_X,
    'outInvertsSweep' : true,
    'scaleIsotropically' : false
};
aTransitionInfoTable[VEEWIPE_TRANSITION][UP_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 180.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_FLIP_Y,
    'outInvertsSweep' : true,
    'scaleIsotropically' : false
};
aTransitionInfoTable[VEEWIPE_TRANSITION][RIGHT_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : -90.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_FLIP_X,
    'outInvertsSweep' : true,
    'scaleIsotropically' : false
};

aTransitionInfoTable[FANWIPE_TRANSITION] = {};
aTransitionInfoTable[FANWIPE_TRANSITION][CENTERTOP_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 0.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_FLIP_Y,
    'outInvertsSweep' : true,
    'scaleIsotropically' : false
};
aTransitionInfoTable[FANWIPE_TRANSITION][CENTERRIGHT_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 90.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_FLIP_X,
    'outInvertsSweep' : true,
    'scaleIsotropically' : false
};
aTransitionInfoTable[FANWIPE_TRANSITION][TOP_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 180.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_FLIP_Y,
    'outInvertsSweep' : true,
    'scaleIsotropically' : false
};
aTransitionInfoTable[FANWIPE_TRANSITION][RIGHT_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : -90.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_FLIP_X,
    'outInvertsSweep' : true,
    'scaleIsotropically' : false
};
aTransitionInfoTable[FANWIPE_TRANSITION][BOTTOM_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 180.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_FLIP_Y,
    'outInvertsSweep' : true,
    'scaleIsotropically' : false
};
aTransitionInfoTable[FANWIPE_TRANSITION][LEFT_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 90.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_FLIP_X,
    'outInvertsSweep' : true,
    'scaleIsotropically' : false
};


aTransitionInfoTable[PINWHEELWIPE_TRANSITION] = {};
aTransitionInfoTable[PINWHEELWIPE_TRANSITION][ONEBLADE_TRANS_SUBTYPE] =
aTransitionInfoTable[PINWHEELWIPE_TRANSITION][TWOBLADEVERTICAL_TRANS_SUBTYPE] =
aTransitionInfoTable[PINWHEELWIPE_TRANSITION][THREEBLADE_TRANS_SUBTYPE] =
aTransitionInfoTable[PINWHEELWIPE_TRANSITION][FOURBLADE_TRANS_SUBTYPE] =
aTransitionInfoTable[PINWHEELWIPE_TRANSITION][EIGHTBLADE_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 0.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_FLIP_X,
    'outInvertsSweep' : true,
    'scaleIsotropically' : true
};
aTransitionInfoTable[PINWHEELWIPE_TRANSITION][TWOBLADEHORIZONTAL_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : -90.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_FLIP_Y,
    'outInvertsSweep' : true,
    'scaleIsotropically' : true
};

aTransitionInfoTable[PUSHWIPE_TRANSITION] = {};
aTransitionInfoTable[PUSHWIPE_TRANSITION][FROMLEFT_TRANS_SUBTYPE] =
aTransitionInfoTable[PUSHWIPE_TRANSITION][FROMTOP_TRANS_SUBTYPE] =
aTransitionInfoTable[PUSHWIPE_TRANSITION][FROMRIGHT_TRANS_SUBTYPE] =
aTransitionInfoTable[PUSHWIPE_TRANSITION][FROMBOTTOM_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_SPECIAL,
    'rotationAngle' : 0.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_IGNORE,
    'outInvertsSweep' : true,
    'scaleIsotropically' : false
};


aTransitionInfoTable[SINGLESWEEPWIPE_TRANSITION] = {};
aTransitionInfoTable[SINGLESWEEPWIPE_TRANSITION][CLOCKWISETOP_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 0.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_SUBTRACT_AND_INVERT,
    'outInvertsSweep' : true,
    'scaleIsotropically' : false
};
aTransitionInfoTable[SINGLESWEEPWIPE_TRANSITION][CLOCKWISERIGHT_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 90.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_SUBTRACT_AND_INVERT,
    'outInvertsSweep' : true,
    'scaleIsotropically' : false
};
aTransitionInfoTable[SINGLESWEEPWIPE_TRANSITION][CLOCKWISEBOTTOM_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 180.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_SUBTRACT_AND_INVERT,
    'outInvertsSweep' : true,
    'scaleIsotropically' : false
};
aTransitionInfoTable[SINGLESWEEPWIPE_TRANSITION][CLOCKWISELEFT_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 270.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_SUBTRACT_AND_INVERT,
    'outInvertsSweep' : true,
    'scaleIsotropically' : false
};
aTransitionInfoTable[SINGLESWEEPWIPE_TRANSITION][CLOCKWISETOPLEFT_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 0.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_SUBTRACT_AND_INVERT,
    'outInvertsSweep' : true,
    'scaleIsotropically' : false
};
aTransitionInfoTable[SINGLESWEEPWIPE_TRANSITION][COUNTERCLOCKWISEBOTTOMLEFT_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 180.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_SUBTRACT_AND_INVERT,
    'outInvertsSweep' : true,
    'scaleIsotropically' : false
};
aTransitionInfoTable[SINGLESWEEPWIPE_TRANSITION][CLOCKWISEBOTTOMRIGHT_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 180.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_SUBTRACT_AND_INVERT,
    'outInvertsSweep' : true,
    'scaleIsotropically' : false
};
aTransitionInfoTable[SINGLESWEEPWIPE_TRANSITION][COUNTERCLOCKWISETOPRIGHT_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 0.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_SUBTRACT_AND_INVERT,
    'outInvertsSweep' : true,
    'scaleIsotropically' : false
};

aTransitionInfoTable[SLIDEWIPE_TRANSITION] = {};
aTransitionInfoTable[SLIDEWIPE_TRANSITION][FROMLEFT_TRANS_SUBTYPE] =
aTransitionInfoTable[SLIDEWIPE_TRANSITION][FROMTOP_TRANS_SUBTYPE] =
aTransitionInfoTable[SLIDEWIPE_TRANSITION][FROMRIGHT_TRANS_SUBTYPE] =
aTransitionInfoTable[SLIDEWIPE_TRANSITION][FROMBOTTOM_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_SPECIAL,
    'rotationAngle' : 0.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_IGNORE,
    'outInvertsSweep' : true,
    'scaleIsotropically' : false
};

aTransitionInfoTable[FADE_TRANSITION] = {};
aTransitionInfoTable[FADE_TRANSITION][CROSSFADE_TRANS_SUBTYPE] =
aTransitionInfoTable[FADE_TRANSITION][FADETOCOLOR_TRANS_SUBTYPE] =
aTransitionInfoTable[FADE_TRANSITION][FADEFROMCOLOR_TRANS_SUBTYPE] =
aTransitionInfoTable[FADE_TRANSITION][FADEOVERCOLOR_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_SPECIAL,
    'rotationAngle' : 0.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_IGNORE,
    'outInvertsSweep' : true,
    'scaleIsotropically' : false
};


aTransitionInfoTable[RANDOMBARWIPE_TRANSITION] = {};
aTransitionInfoTable[RANDOMBARWIPE_TRANSITION][VERTICAL_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 90.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_IGNORE,
    'outInvertsSweep' : true,
    'scaleIsotropically' : false
};
aTransitionInfoTable[RANDOMBARWIPE_TRANSITION][HORIZONTAL_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 0.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_IGNORE,
    'outInvertsSweep' : true,
    'scaleIsotropically' : false
};

aTransitionInfoTable[CHECKERBOARDWIPE_TRANSITION] = {};
aTransitionInfoTable[CHECKERBOARDWIPE_TRANSITION][DOWN_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 90.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_FLIP_Y,
    'outInvertsSweep' : true,
    'scaleIsotropically' : false
};
aTransitionInfoTable[CHECKERBOARDWIPE_TRANSITION][ACROSS_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 0.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_FLIP_X,
    'outInvertsSweep' : true,
    'scaleIsotropically' : false
};

aTransitionInfoTable[DISSOLVE_TRANSITION] = {};
aTransitionInfoTable[DISSOLVE_TRANSITION][DEFAULT_TRANS_SUBTYPE] =
{
    'class' : TRANSITION_CLIP_POLYPOLYGON,
    'rotationAngle' : 0.0,
    'scaleX' : 1.0,
    'scaleY' : 1.0,
    'reverseMethod' : REVERSEMETHOD_IGNORE,
    'outInvertsSweep' : true,
    'scaleIsotropically' : true
};


// Transition tables

function createStateTransitionTable()
{
    var aSTT = {};

    aSTT[RESTART_MODE_NEVER] = {};
    aSTT[RESTART_MODE_WHEN_NOT_ACTIVE] = {};
    aSTT[RESTART_MODE_ALWAYS] = {};

    // transition table for restart=NEVER, fill=REMOVE
    var aTable =
    aSTT[RESTART_MODE_NEVER][FILL_MODE_REMOVE] = {};
    aTable[INVALID_NODE]        = INVALID_NODE;
    aTable[UNRESOLVED_NODE]     = RESOLVED_NODE | ENDED_NODE;
    aTable[RESOLVED_NODE]       = ACTIVE_NODE | ENDED_NODE;
    aTable[ACTIVE_NODE]         = ENDED_NODE;
    aTable[FROZEN_NODE]         = INVALID_NODE;  // this state is unreachable here
    aTable[ENDED_NODE]          = ENDED_NODE;    // this state is a sink here (cannot restart)

    // transition table for restart=NEVER, fill=FREEZE
    aTable =
    aSTT[RESTART_MODE_NEVER][FILL_MODE_FREEZE] =
    aSTT[RESTART_MODE_NEVER][FILL_MODE_HOLD] =
    aSTT[RESTART_MODE_NEVER][FILL_MODE_TRANSITION] = {};
    aTable[INVALID_NODE]        = INVALID_NODE;
    aTable[UNRESOLVED_NODE]     = RESOLVED_NODE | ENDED_NODE;
    aTable[RESOLVED_NODE]       = ACTIVE_NODE | ENDED_NODE;
    aTable[ACTIVE_NODE]         = FROZEN_NODE | ENDED_NODE;
    aTable[FROZEN_NODE]         = ENDED_NODE;
    aTable[ENDED_NODE]          = ENDED_NODE;   // this state is a sink here (cannot restart)

    // transition table for restart=WHEN_NOT_ACTIVE, fill=REMOVE
    aTable =
    aSTT[RESTART_MODE_WHEN_NOT_ACTIVE][FILL_MODE_REMOVE] = {};
    aTable[INVALID_NODE]        = INVALID_NODE;
    aTable[UNRESOLVED_NODE]     = RESOLVED_NODE | ENDED_NODE;
    aTable[RESOLVED_NODE]       = ACTIVE_NODE | ENDED_NODE;
    aTable[ACTIVE_NODE]         = ENDED_NODE;
    aTable[FROZEN_NODE]         = INVALID_NODE;  // this state is unreachable here
    aTable[ENDED_NODE]          = RESOLVED_NODE | ACTIVE_NODE | ENDED_NODE;  // restart is possible

    // transition table for restart=WHEN_NOT_ACTIVE, fill=FREEZE
    aTable =
    aSTT[RESTART_MODE_WHEN_NOT_ACTIVE][FILL_MODE_FREEZE] =
    aSTT[RESTART_MODE_WHEN_NOT_ACTIVE][FILL_MODE_HOLD] =
    aSTT[RESTART_MODE_WHEN_NOT_ACTIVE][FILL_MODE_TRANSITION] = {};
    aTable[INVALID_NODE]        = INVALID_NODE;
    aTable[UNRESOLVED_NODE]     = RESOLVED_NODE | ENDED_NODE;
    aTable[RESOLVED_NODE]       = ACTIVE_NODE | ENDED_NODE;
    aTable[ACTIVE_NODE]         = FROZEN_NODE | ENDED_NODE;
    aTable[FROZEN_NODE]         = RESOLVED_NODE | ACTIVE_NODE | ENDED_NODE;  // restart is possible
    aTable[ENDED_NODE]          = RESOLVED_NODE | ACTIVE_NODE | ENDED_NODE;  // restart is possible

    // transition table for restart=ALWAYS, fill=REMOVE
    aTable =
    aSTT[RESTART_MODE_ALWAYS][FILL_MODE_REMOVE] = {};
    aTable[INVALID_NODE]        = INVALID_NODE;
    aTable[UNRESOLVED_NODE]     = RESOLVED_NODE | ENDED_NODE;
    aTable[RESOLVED_NODE]       = ACTIVE_NODE | ENDED_NODE;
    aTable[ACTIVE_NODE]         = RESOLVED_NODE | ACTIVE_NODE | ENDED_NODE;  // restart is possible
    aTable[FROZEN_NODE]         = INVALID_NODE;  // this state is unreachable here
    aTable[ENDED_NODE]          = RESOLVED_NODE | ACTIVE_NODE | ENDED_NODE;  // restart is possible

    // transition table for restart=ALWAYS, fill=FREEZE
    aTable =
    aSTT[RESTART_MODE_ALWAYS][FILL_MODE_FREEZE] =
    aSTT[RESTART_MODE_ALWAYS][FILL_MODE_HOLD] =
    aSTT[RESTART_MODE_ALWAYS][FILL_MODE_TRANSITION] = {};
    aTable[INVALID_NODE]        = INVALID_NODE;
    aTable[UNRESOLVED_NODE]     = RESOLVED_NODE | ENDED_NODE;
    aTable[RESOLVED_NODE]       = ACTIVE_NODE | ENDED_NODE;
    aTable[ACTIVE_NODE]         = RESOLVED_NODE | ACTIVE_NODE | FROZEN_NODE | ENDED_NODE;
    aTable[FROZEN_NODE]         = RESOLVED_NODE | ACTIVE_NODE | ENDED_NODE;  // restart is possible
    aTable[ENDED_NODE]          = RESOLVED_NODE | ACTIVE_NODE | ENDED_NODE;  // restart is possible

    return aSTT;
}

var aStateTransitionTable = createStateTransitionTable();



function getTransitionTable( eRestartMode, eFillMode )
{
    // If restart mode has not been resolved we use 'never'.
    // Note: RESTART_MODE_DEFAULT == RESTART_MODE_INHERIT.
    if( eRestartMode == RESTART_MODE_DEFAULT )
    {
        log( 'getTransitionTable: unexpected restart mode: ' + eRestartMode
                 + '. Used NEVER instead.');
        eRestartMode = RESTART_MODE_NEVER;
    }

    // If fill mode has not been resolved we use 'remove'.
    // Note: FILL_MODE_DEFAULT == FILL_MODE_INHERIT
    if( eFillMode == FILL_MODE_DEFAULT ||
        eFillMode == FILL_MODE_AUTO )
    {
        eFillMode = FILL_MODE_REMOVE;
    }

    return aStateTransitionTable[eRestartMode][eFillMode];
}





// Event Triggers
var EVENT_TRIGGER_UNKNOWN               = 0;
var EVENT_TRIGGER_ON_SLIDE_BEGIN        = 1; // eslint-disable-line no-unused-vars
var EVENT_TRIGGER_ON_SLIDE_END          = 2; // eslint-disable-line no-unused-vars
var EVENT_TRIGGER_BEGIN_EVENT           = 3;
var EVENT_TRIGGER_END_EVENT             = 4;
var EVENT_TRIGGER_ON_CLICK              = 5;
var EVENT_TRIGGER_ON_DBL_CLICK          = 6; // eslint-disable-line no-unused-vars
var EVENT_TRIGGER_ON_MOUSE_ENTER        = 7; // eslint-disable-line no-unused-vars
var EVENT_TRIGGER_ON_MOUSE_LEAVE        = 8; // eslint-disable-line no-unused-vars
var EVENT_TRIGGER_ON_NEXT_EFFECT        = 9;
var EVENT_TRIGGER_ON_PREV_EFFECT        = 10;
var EVENT_TRIGGER_REPEAT                = 11; // eslint-disable-line no-unused-vars

var aEventTriggerOutMap = [ 'unknown', 'slideBegin', 'slideEnd', 'begin', 'end', 'click',
                            'doubleClick', 'mouseEnter', 'mouseLeave', 'next', 'previous', 'repeat' ];


function getEventTriggerType( sEventTrigger )
{
    if( sEventTrigger == 'begin' )
        return EVENT_TRIGGER_BEGIN_EVENT;
    else if( sEventTrigger == 'end' )
        return EVENT_TRIGGER_END_EVENT;
    else if( sEventTrigger == 'next' )
        return EVENT_TRIGGER_ON_NEXT_EFFECT;
    else if( sEventTrigger == 'prev' )
        return EVENT_TRIGGER_ON_PREV_EFFECT;
    else if( sEventTrigger == 'click' )
        return EVENT_TRIGGER_ON_CLICK;
    else
        return EVENT_TRIGGER_UNKNOWN;
}





// Timing Types
var UNKNOWN_TIMING          = 0;
var OFFSET_TIMING           = 1;
var WALLCLOCK_TIMING        = 2; // eslint-disable-line no-unused-vars
var INDEFINITE_TIMING       = 3;
var EVENT_TIMING            = 4;
var SYNCBASE_TIMING         = 5;
var MEDIA_TIMING            = 6; // eslint-disable-line no-unused-vars

var aTimingTypeOutMap = [ 'unknown', 'offset', 'wallclock', 'indefinite', 'event', 'syncbase', 'media' ];


// Char Codes
var CHARCODE_PLUS       = '+'.charCodeAt(0);
var CHARCODE_MINUS      = '-'.charCodeAt(0);
var CHARCODE_0          = '0'.charCodeAt(0);
var CHARCODE_9          = '9'.charCodeAt(0);



function Timing( aAnimationNode, sTimingAttribute )
{
    this.aAnimationNode = aAnimationNode;     // the node, the timing attribute belongs to
    this.sTimingDescription = removeWhiteSpaces( sTimingAttribute );
    this.eTimingType = UNKNOWN_TIMING;
    this.nOffset = 0.0;                       // in seconds
    this.sEventBaseElementId = '';            // the element id for event based timing
    this.eEventType = EVENT_TRIGGER_UNKNOWN;  // the event type
}

Timing.prototype.getAnimationNode = function()
{
    return this.aAnimationNode;
};

Timing.prototype.getType = function()
{
    return this.eTimingType;
};

Timing.prototype.getOffset = function()
{
    return this.nOffset;
};

Timing.prototype.getEventBaseElementId = function()
{
    return this.sEventBaseElementId;
};

Timing.prototype.getEventType = function()
{
    return this.eEventType;
};

Timing.prototype.parse = function()
{
    if( !this.sTimingDescription )
    {
        this.eTimingType = OFFSET_TIMING;
        return;
    }

    if( this.sTimingDescription == 'indefinite' )
        this.eTimingType = INDEFINITE_TIMING;
    else
    {
        var nFirstCharCode = this.sTimingDescription.charCodeAt(0);
        var bPositiveOffset = !( nFirstCharCode == CHARCODE_MINUS );
        if ( ( nFirstCharCode == CHARCODE_PLUS ) ||
             ( nFirstCharCode == CHARCODE_MINUS ) ||
             ( ( nFirstCharCode >= CHARCODE_0 ) && ( nFirstCharCode <= CHARCODE_9 ) ) )
        {
            var sClockValue
                = ( ( nFirstCharCode == CHARCODE_PLUS ) || ( nFirstCharCode == CHARCODE_MINUS ) )
                    ? this.sTimingDescription.substr( 1 )
                    : this.sTimingDescription;

            var TimeInSec = Timing.parseClockValue( sClockValue );
            if( TimeInSec != undefined )
            {
                this.eTimingType = OFFSET_TIMING;
                this.nOffset = bPositiveOffset ? TimeInSec : -TimeInSec;
            }
        }
        else
        {
            var aTimingSplit = [];
            bPositiveOffset = true;
            if( this.sTimingDescription.indexOf( '+' ) != -1 )
            {
                aTimingSplit = this.sTimingDescription.split( '+' );
            }
            else if( this.sTimingDescription.indexOf( '-' ) != -1 )
            {
                aTimingSplit = this.sTimingDescription.split( '-' );
                bPositiveOffset = false;
            }
            else
            {
                aTimingSplit[0] = this.sTimingDescription;
                aTimingSplit[1] = '';
            }

            if( aTimingSplit[0].indexOf( '.' ) != -1 )
            {
                var aEventSplit = aTimingSplit[0].split( '.' );
                this.sEventBaseElementId = aEventSplit[0];
                this.eEventType = getEventTriggerType( aEventSplit[1] );
            }
            else
            {
                this.eEventType = getEventTriggerType( aTimingSplit[0] );
            }

            if( this.eEventType == EVENT_TRIGGER_UNKNOWN )
                return;

            if( ( this.eEventType == EVENT_TRIGGER_BEGIN_EVENT ) ||
                ( this.eEventType == EVENT_TRIGGER_END_EVENT ) )
            {
                this.eTimingType = SYNCBASE_TIMING;
            }
            else
            {
                this.eTimingType = EVENT_TIMING;
            }

            if( aTimingSplit[1] )
            {
                sClockValue = aTimingSplit[1];
                TimeInSec = Timing.parseClockValue( sClockValue );
                if( TimeInSec != undefined )
                {
                    this.nOffset = ( bPositiveOffset ) ? TimeInSec : -TimeInSec;
                }
                else
                {
                    this.eTimingType = UNKNOWN_TIMING;
                }

            }
        }
    }

};

Timing.parseClockValue = function( sClockValue )
{
    if( !sClockValue )
        return 0.0;

    var nTimeInSec = undefined;

    var reFullClockValue = /^([0-9]+):([0-5][0-9]):([0-5][0-9])(.[0-9]+)?$/;
    var rePartialClockValue = /^([0-5][0-9]):([0-5][0-9])(.[0-9]+)?$/;
    var reTimeCountValue = /^([0-9]+)(.[0-9]+)?(h|min|s|ms)?$/;

    if( reFullClockValue.test( sClockValue ) )
    {
        var aClockTimeParts = reFullClockValue.exec( sClockValue );

        var nHours = parseInt( aClockTimeParts[1] );
        var nMinutes = parseInt( aClockTimeParts[2] );
        var nSeconds = parseInt( aClockTimeParts[3] );
        if( aClockTimeParts[4] )
            nSeconds += parseFloat( aClockTimeParts[4] );

        nTimeInSec = ( ( nHours * 60 ) +  nMinutes ) * 60 + nSeconds;

    }
    else if( rePartialClockValue.test( sClockValue ) )
    {
        aClockTimeParts = rePartialClockValue.exec( sClockValue );

        nMinutes = parseInt( aClockTimeParts[1] );
        nSeconds = parseInt( aClockTimeParts[2] );
        if( aClockTimeParts[3] )
            nSeconds += parseFloat( aClockTimeParts[3] );

        nTimeInSec = nMinutes * 60 + nSeconds;
    }
    else if( reTimeCountValue.test( sClockValue ) )
    {
        aClockTimeParts = reTimeCountValue.exec( sClockValue );

        var nTimeCount = parseInt( aClockTimeParts[1] );
        if( aClockTimeParts[2] )
            nTimeCount += parseFloat( aClockTimeParts[2] );

        if( aClockTimeParts[3] )
        {
            if( aClockTimeParts[3] == 'h' )
            {
                nTimeInSec = nTimeCount * 3600;
            }
            else if( aClockTimeParts[3] == 'min' )
            {
                nTimeInSec = nTimeCount * 60;
            }
            else if( aClockTimeParts[3] == 's' )
            {
                nTimeInSec = nTimeCount;
            }
            else if( aClockTimeParts[3] == 'ms' )
            {
                nTimeInSec = nTimeCount / 1000;
            }
        }
        else
        {
            nTimeInSec = nTimeCount;
        }

    }

    if( nTimeInSec )
        nTimeInSec = parseFloat( nTimeInSec.toFixed( 3 ) );
    return nTimeInSec;
};

Timing.prototype.info = function( bVerbose )
{

    var sInfo = '';

    if( bVerbose )
    {
        sInfo = 'description: ' + this.sTimingDescription + ', ';

        sInfo += ', type: ' +  aTimingTypeOutMap[ this.getType() ];
        sInfo += ', offset: ' + this.getOffset();
        sInfo += ', event base element id: ' + this.getEventBaseElementId();
        sInfo += ', timing event type: ' + aEventTriggerOutMap[ this.getEventType() ];
    }
    else
    {
        switch( this.getType() )
        {
            case INDEFINITE_TIMING:
                sInfo += 'indefinite';
                break;
            case OFFSET_TIMING:
                sInfo += this.getOffset();
                break;
            case EVENT_TIMING:
            case SYNCBASE_TIMING:
                if( this.getEventBaseElementId() )
                    sInfo += this.getEventBaseElementId() + '.';
                sInfo += aEventTriggerOutMap[ this.getEventType() ];
                if( this.getOffset() )
                {
                    if( this.getOffset() > 0 )
                        sInfo += '+';
                    sInfo += this.getOffset();
                }
        }
    }

    return sInfo;
};




function Duration( sDurationAttribute )
{
    this.bIndefinite = false;
    this.bMedia = false;
    this.nValue = undefined;
    this.bDefined = false;

    if( !sDurationAttribute )
        return;

    if( sDurationAttribute == 'indefinite' )
        this.bIndefinite = true;
    else if( sDurationAttribute == 'media' )
        this.bMedia = true;
    else
    {
        this.nValue = Timing.parseClockValue( sDurationAttribute );
        if( this.nValue <= 0.0 )
            this.nValue = 0.001;  // duration must be always greater than 0
    }
    this.bDefined = true;
}


Duration.prototype.isSet = function()
{
    return this.bDefined;
};

Duration.prototype.isIndefinite = function()
{
    return this.bIndefinite;
};

Duration.prototype.isMedia = function()
{
    return this.bMedia;
};

Duration.prototype.isValue = function()
{
    return this.nValue != undefined;
};

Duration.prototype.getValue= function()
{
    return this.nValue;
};

Duration.prototype.info= function()
{
    var sInfo;

    if( this.isIndefinite() )
        sInfo = 'indefinite';
    else if( this.isMedia() )
        sInfo = 'media';
    else if( this.getValue() )
        sInfo = this.getValue();

    return sInfo;
};




function AnimationNode()
{
}

AnimationNode.prototype.init = function() {};
AnimationNode.prototype.resolve = function() {};
AnimationNode.prototype.activate = function() {};
AnimationNode.prototype.deactivate = function() {};
AnimationNode.prototype.end = function() {};
AnimationNode.prototype.getState = function() {};
AnimationNode.prototype.registerDeactivatingListener = function() {};
AnimationNode.prototype.notifyDeactivating = function() {};




function NodeContext( aSlideShowContext )
{
    this.aContext = aSlideShowContext;
    this.aAnimationNodeMap = null;
    this.aAnimatedElementMap = null;
    this.aSourceEventElementMap = null;
    this.nStartDelay = 0.0;
    this.bFirstRun = undefined;
    this.bIsInvalid = false;
    this.aSlideHeight = HEIGHT;
    this.aSlideWidth = WIDTH;
}


NodeContext.prototype.makeSourceEventElement = function( sId, aEventBaseElem )
{
    if( !aEventBaseElem )
    {
        log( 'NodeContext.makeSourceEventElement: event base element is not valid' );
        return null;
    }

    if( !this.aContext.aEventMultiplexer )
    {
        log( 'NodeContext.makeSourceEventElement: event multiplexer not initialized' );
        return null;
    }

    if( !this.aSourceEventElementMap[ sId ] )
    {
        this.aSourceEventElementMap[ sId ] = new SourceEventElement( sId, aEventBaseElem, this.aContext.aEventMultiplexer );
    }
    return this.aSourceEventElementMap[ sId ];
};




function StateTransition( aBaseNode )
{
    this.aNode = aBaseNode;
    this.eToState = INVALID_NODE;
}

StateTransition.prototype.enter = function( eNodeState, bForce )
{
    if( !bForce ) bForce = false;

    if( this.eToState != INVALID_NODE )
    {
        log( 'StateTransition.enter: commit() before enter()ing again!' );
        return false;
    }
    if( !bForce && !this.aNode.isTransition( this.aNode.getState(), eNodeState  ) )
        return false;

    // recursion detection:
    if( ( this.aNode.nCurrentStateTransition & eNodeState ) != 0 )
        return false; // already in wanted transition

    // mark transition:
    this.aNode.nCurrentStateTransition |= eNodeState;
    this.eToState = eNodeState;
    return true;
};

StateTransition.prototype.commit = function()
{
    if( this.eToState != INVALID_NODE )
    {
        this.aNode.eCurrentState = this.eToState;
        this.clear();
    }
};

StateTransition.prototype.clear = function()
{
    if( this.eToState != INVALID_NODE )
    {
        this.aNode.nCurrentStateTransition &= ~this.eToState;
        this.eToState = INVALID_NODE;
    }
};




function BaseNode( aAnimElem, aParentNode, aNodeContext )
{
    this.nId = getUniqueId();
    this.sClassName = 'BaseNode';

    if( !aAnimElem )
        log( 'BaseNode(id:' + this.nId + ') constructor: aAnimElem is not valid' );

    if( !aNodeContext )
        log( 'BaseNode(id:' + this.nId + ') constructor: aNodeContext is not valid' );

    if( !aNodeContext.aContext )
        log( 'BaseNode(id:' + this.nId + ') constructor: aNodeContext.aContext is not valid' );


    this.bIsContainer = false;
    this.aElement = aAnimElem;
    this.aParentNode = aParentNode;
    this.aNodeContext = aNodeContext;
    this.aContext = aNodeContext.aContext;
    this.nStartDelay = aNodeContext.nStartDelay;
    this.eCurrentState = UNRESOLVED_NODE;
    this.nCurrentStateTransition = 0;
    this.aDeactivatingListenerArray = [];
    this.aActivationEvent = null;
    this.aDeactivationEvent = null;

    this.aBegin = null;
    this.aDuration = null;
    this.aEnd = null;
    this.bMainSequenceRootNode = false;
    this.bInteractiveSequenceRootNode = false;
    this.eFillMode = FILL_MODE_FREEZE;
    this.eRestartMode = RESTART_MODE_NEVER;
    this.nRepeatCount = undefined;
    this.nAccelerate = 0.0;
    this.nDecelerate = 0.0;
    this.bAutoReverse = false;

}
extend( BaseNode, AnimationNode );


BaseNode.prototype.getId = function()
{
    return this.nId;
};

BaseNode.prototype.parseElement = function()
{
    var aAnimElem = this.aElement;

    // id attribute
    var sIdAttr = aAnimElem.getAttributeNS( NSS['xml'], 'id' );
    // we append the animation node to the Animation Node Map so that it can be retrieved
    // by the registerEvent routine for resolving begin values of type 'id.begin', 'id.end'
    if( sIdAttr )
        this.aNodeContext.aAnimationNodeMap[ sIdAttr ] = this;

    // begin attribute
    this.aBegin = null;
    var sBeginAttr = aAnimElem.getAttributeNS( NSS['smil'], 'begin' );
    this.aBegin = new Timing( this, sBeginAttr );
    this.aBegin.parse();

    // end attribute
    this.aEnd = null;
    var sEndAttr = aAnimElem.getAttributeNS( NSS['smil'], 'end' );
    if( sEndAttr )
    {
        this.aEnd = new Timing( this, sEndAttr );
        this.aEnd.parse();
    }

    // dur attribute
    this.aDuration = null;
    var sDurAttr = aAnimElem.getAttributeNS( NSS['smil'], 'dur' );
    this.aDuration = new Duration( sDurAttr );
    if( !this.aDuration.isSet() )
    {
        if( this.isContainer() )
            this.aDuration = null;
        else
            this.aDuration = new Duration( 'indefinite' );
    }

    // fill attribute
    var sFillAttr = aAnimElem.getAttributeNS( NSS['smil'], 'fill' );
    if( sFillAttr && aFillModeInMap[ sFillAttr ])
        this.eFillMode = aFillModeInMap[ sFillAttr ];
    else
        this.eFillMode = FILL_MODE_DEFAULT;

    // restart attribute
    var sRestartAttr = aAnimElem.getAttributeNS( NSS['smil'], 'restart' );
    if( sRestartAttr && aRestartModeInMap[ sRestartAttr ] )
        this.eRestartMode = aRestartModeInMap[ sRestartAttr ];
    else
        this.eRestartMode = RESTART_MODE_DEFAULT;

    // repeatCount attribute
    var sRepeatCount = aAnimElem.getAttributeNS( NSS['smil'], 'repeatCount' );
    if( !sRepeatCount )
        this.nRepeatCount = 1;
    else
        this.nRepeatCount = parseFloat( sRepeatCount );
    if( ( isNaN(this.nRepeatCount) ) && ( sRepeatCount != 'indefinite' ) )
        this.nRepeatCount = 1;

    // accelerate attribute
    this.nAccelerate = 0.0;
    var sAccelerateAttr = aAnimElem.getAttributeNS( NSS['smil'], 'accelerate' );
    if( sAccelerateAttr )
        this.nAccelerate = parseFloat( sAccelerateAttr );
    if( isNaN(this.nAccelerate) )
        this.nAccelerate = 0.0;

    // decelerate attribute
    this.nDecelerate = 0.0;
    var sDecelerateAttr = aAnimElem.getAttributeNS( NSS['smil'], 'decelerate' );
    if( sDecelerateAttr )
        this.nDecelerate = parseFloat( sDecelerateAttr );
    if( isNaN(this.nDecelerate) )
        this.nDecelerate = 0.0;

    // autoReverse attribute
    this.bAutoreverse = false;
    var sAutoReverseAttr = aAnimElem.getAttributeNS( NSS['smil'], 'autoReverse' );
    if( sAutoReverseAttr == 'true' )
        this.bAutoreverse = true;


    // resolve fill value
    if( this.eFillMode == FILL_MODE_DEFAULT )
        if( this.getParentNode() )
            this.eFillMode = this.getParentNode().getFillMode();
        else
            this.eFillMode = FILL_MODE_AUTO;

    if( this.eFillMode ==  FILL_MODE_AUTO ) // see SMIL recommendation document
    {
        this.eFillMode = ( this.aEnd ||
                           ( this.nRepeatCount != 1) ||
                           ( this.aDuration && !this.aDuration.isIndefinite() ) )
                              ? FILL_MODE_REMOVE
                              : FILL_MODE_FREEZE;
    }

    // resolve restart value
    if( this.eRestartMode == RESTART_MODE_DEFAULT )
        if( this.getParentNode() )
            this.eRestartMode = this.getParentNode().getRestartMode();
        else
            // SMIL recommendation document says to set it to 'always'
            this.eRestartMode = RESTART_MODE_ALWAYS;

    // resolve accelerate and decelerate attributes
    // from the SMIL recommendation document: if the individual values of the accelerate
    // and decelerate attributes are between 0 and 1 and the sum is greater than 1,
    // then both the accelerate and decelerate attributes will be ignored and the timed
    // element will behave as if neither attribute was specified.
    if( ( this.nAccelerate + this.nDecelerate ) > 1.0 )
    {
        this.nAccelerate = 0.0;
        this.nDecelerate = 0.0;
    }

    this.aStateTransTable = getTransitionTable( this.getRestartMode(), this.getFillMode() );

    return true;
};

BaseNode.prototype.getParentNode = function()
{
    return this.aParentNode;
};

BaseNode.prototype.init = function()
{
    this.DBG( this.callInfo( 'init' ) );
    if( ! this.checkValidNode() )
        return false;
    if( this.aActivationEvent )
        this.aActivationEvent.dispose();
    if( this.aDeactivationEvent )
        this.aDeactivationEvent.dispose();

    this.eCurrentState = UNRESOLVED_NODE;

    return this.init_st();
};

BaseNode.prototype.resolve = function()
{
    if( this.aNodeContext.bIsInvalid || ! this.checkValidNode() )
        return false;

    this.DBG( this.callInfo( 'resolve' ) );

    if( this.eCurrentState == RESOLVED_NODE )
        log( 'BaseNode.resolve: already in RESOLVED state' );

    var aStateTrans = new StateTransition( this );

    if( aStateTrans.enter( RESOLVED_NODE ) &&
        this.isTransition( RESOLVED_NODE, ACTIVE_NODE ) &&
        this.resolve_st() )
    {
        aStateTrans.commit();

        if( this.aActivationEvent )
        {
            this.aActivationEvent.charge();
        }
        else
        {
            this.aActivationEvent = makeDelay( bind( this, this.activate ), this.getBegin().getOffset() + this.nStartDelay );
        }
        registerEvent( this.getId(), this.getBegin(), this.aActivationEvent, this.aNodeContext );

        return true;
    }

    return false;
};

BaseNode.prototype.activate = function()
{
    if( ! this.checkValidNode() )
        return false;

    if( this.eCurrentState == ACTIVE_NODE )
        log( 'BaseNode.activate: already in ACTIVE state' );

    this.DBG( this.callInfo( 'activate' ), getCurrentSystemTime() );

    var aStateTrans = new StateTransition( this );

    if( aStateTrans.enter( ACTIVE_NODE ) )
    {
        this.activate_st();
        aStateTrans.commit();
        if( !this.aContext.aEventMultiplexer )
            log( 'BaseNode.activate: this.aContext.aEventMultiplexer is not valid' );
        this.aContext.aEventMultiplexer.notifyEvent( EVENT_TRIGGER_BEGIN_EVENT, this.getId() );
        return true;
    }
    return false;
};

BaseNode.prototype.deactivate = function()
{
    if( this.inStateOrTransition( ENDED_NODE | FROZEN_NODE ) || !this.checkValidNode() )
        return;

    if( this.isTransition( this.eCurrentState, FROZEN_NODE ) )
    {
        this.DBG( this.callInfo( 'deactivate' ), getCurrentSystemTime() );

        var aStateTrans = new StateTransition( this );
        if( aStateTrans.enter( FROZEN_NODE, true /* FORCE */ ) )
        {
            this.deactivate_st( FROZEN_NODE );
            aStateTrans.commit();

            this.notifyEndListeners();

            if( this.aActivationEvent )
                this.aActivationEvent.dispose();
            if( this.aDeactivationEvent )
                this.aDeactivationEvent.dispose();
        }
    }
    else
    {
        this.end();
    }
    // state has changed either to FROZEN or ENDED
};

BaseNode.prototype.end = function()
{
    var bIsFrozenOrInTransitionToFrozen = this.inStateOrTransition( FROZEN_NODE );
    if( this.inStateOrTransition( ENDED_NODE ) || !this.checkValidNode() )
        return;

    if( !(this.isTransition( this.eCurrentState, ENDED_NODE ) ) )
        log( 'BaseNode.end: end state not reachable in transition table' );

    this.DBG( this.callInfo( 'end' ), getCurrentSystemTime() );

    var aStateTrans = new StateTransition( this );
    if( aStateTrans.enter( ENDED_NODE, true /* FORCE */ ) )
    {
        this.deactivate_st( ENDED_NODE );
        aStateTrans.commit();

        // if is FROZEN or is to be FROZEN, then
        // will/already notified deactivating listeners
        if( !bIsFrozenOrInTransitionToFrozen )
            this.notifyEndListeners();

        if( this.aActivationEvent )
            this.aActivationEvent.dispose();
        if( this.aDeactivationEvent )
            this.aDeactivationEvent.dispose();
    }
};

BaseNode.prototype.dispose = function()
{
    if( this.aActivationEvent )
        this.aActivationEvent.dispose();
    if( this.aDeactivationEvent )
        this.aDeactivationEvent.dispose();
    this.aDeactivatingListenerArray = [];
};

BaseNode.prototype.getState = function()
{
    return this.eCurrentState;
};

BaseNode.prototype.registerDeactivatingListener = function( aNotifiee )
{
    if (! this.checkValidNode())
        return false;

    if( !aNotifiee )
    {
        log( 'BaseNode.registerDeactivatingListener(): invalid notifiee' );
        return false;
    }
    this.aDeactivatingListenerArray.push( aNotifiee );

    return true;
};

BaseNode.prototype.notifyDeactivating = function( aNotifier )
{
    assert( ( aNotifier.getState() == FROZEN_NODE ) || ( aNotifier.getState() == ENDED_NODE ),
            'BaseNode.notifyDeactivating: Notifier node is neither in FROZEN nor in ENDED state' );
};

BaseNode.prototype.isMainSequenceRootNode = function()
{
    return this.bMainSequenceRootNode;
};

BaseNode.prototype.isInteractiveSequenceRootNode = function()
{
    return this.bInteractiveSequenceRootNode;
};

BaseNode.prototype.makeDeactivationEvent = function( nDelay )
{
    if( this.aDeactivationEvent )
    {
        this.aDeactivationEvent.charge();
    }
    else
    {
        if( typeof( nDelay ) == typeof(0) )
            this.aDeactivationEvent = makeDelay( bind( this, this.deactivate ), nDelay );
        else
            this.aDeactivationEvent = null;
    }
    return this.aDeactivationEvent;
};

BaseNode.prototype.scheduleDeactivationEvent = function( aEvent )
{
    this.DBG( this.callInfo( 'scheduleDeactivationEvent' ) );

    if( !aEvent )
    {
        if( this.getDuration() && this.getDuration().isValue() )
            aEvent = this.makeDeactivationEvent( this.getDuration().getValue() );
    }
    if( aEvent )
    {
        this.aContext.aTimerEventQueue.addEvent( aEvent );
    }
};

BaseNode.prototype.checkValidNode = function()
{
    return ( this.eCurrentState != INVALID_NODE );
};

BaseNode.prototype.init_st = function()
{
    return true;
};

BaseNode.prototype.resolve_st = function()
{
    return true;
};

BaseNode.prototype.activate_st = function()
{
    this.scheduleDeactivationEvent();
};

BaseNode.prototype.deactivate_st = function( /*aNodeState*/ )
{
    // empty body
};

BaseNode.prototype.notifyEndListeners = function()
{
    var nDeactivatingListenerCount = this.aDeactivatingListenerArray.length;

    for( var i = 0; i < nDeactivatingListenerCount; ++i )
    {
        this.aDeactivatingListenerArray[i].notifyDeactivating( this );
    }

    this.aContext.aEventMultiplexer.notifyEvent( EVENT_TRIGGER_END_EVENT, this.getId() );
    if( this.getParentNode() && this.getParentNode().isMainSequenceRootNode() )
        this.aContext.aEventMultiplexer.notifyNextEffectEndEvent();

    if( this.isMainSequenceRootNode() )
        this.aContext.aEventMultiplexer.notifyAnimationsEndEvent();
};

BaseNode.prototype.getContext = function()
{
    return this.aContext;
};

BaseNode.prototype.isTransition = function( eFromState, eToState )
{
    return ( ( this.aStateTransTable[ eFromState ] & eToState ) != 0 );
};

BaseNode.prototype.inStateOrTransition = function( nMask )
{
    return ( ( ( this.eCurrentState & nMask ) != 0 ) || ( ( this.nCurrentStateTransition & nMask ) != 0 ) );
};

BaseNode.prototype.isContainer = function()
{
    return this.bIsContainer;
};

BaseNode.prototype.getBegin = function()
{
    return this.aBegin;
};

BaseNode.prototype.getDuration = function()
{
    return this.aDuration;
};

BaseNode.prototype.getEnd = function()
{
    return this.aEnd;
};

BaseNode.prototype.getFillMode = function()
{
    return this.eFillMode;
};

BaseNode.prototype.getRestartMode = function()
{
    return this.eRestartMode;
};

BaseNode.prototype.getRepeatCount = function()
{
    return this.nRepeatCount;
};

BaseNode.prototype.getAccelerateValue = function()
{
    return this.nAccelerate;
};

BaseNode.prototype.getDecelerateValue = function()
{
    return this.nDecelerate;
};

BaseNode.prototype.isAutoReverseEnabled = function()
{
    return this.bAutoreverse;
};

BaseNode.prototype.info = function( bVerbose )
{
    var sInfo = 'class name: ' + this.sClassName;
    sInfo += ';  element name: ' + this.aElement.localName;
    sInfo += ';  id: ' + this.getId();
    sInfo += ';  state: ' + getNodeStateName( this.getState() );

    if( bVerbose )
    {
        // is container
        sInfo += ';  is container: ' + this.isContainer();

        // begin
        if( this.getBegin() )
            sInfo += ';  begin: ' + this.getBegin().info();

        // duration
        if( this.getDuration() )
            sInfo += ';  dur: ' + this.getDuration().info();

        // end
        if( this.getEnd() )
            sInfo += ';  end: ' + this.getEnd().info();

        // fill mode
        if( this.getFillMode() )
            sInfo += ';  fill: ' + aFillModeOutMap[ this.getFillMode() ];

        // restart mode
        if( this.getRestartMode() )
            sInfo += ';  restart: ' + aRestartModeOutMap[ this.getRestartMode() ];

        // repeatCount
        if( this.getRepeatCount() && ( this.getRepeatCount() != 1.0 ) )
            sInfo += ';  repeatCount: ' + this.getRepeatCount();

        // accelerate
        if( this.getAccelerateValue() )
            sInfo += ';  accelerate: ' + this.getAccelerateValue();

        // decelerate
        if( this.getDecelerateValue() )
            sInfo += ';  decelerate: ' + this.getDecelerateValue();

        // auto reverse
        if( this.isAutoReverseEnabled() )
            sInfo += ';  autoReverse: true';

    }

    return sInfo;
};

BaseNode.prototype.callInfo = function( sMethodName )
{
    var sInfo = this.sClassName +
                '( ' + this.getId() +
                ', ' + getNodeStateName( this.getState() ) +
                ' ).' + sMethodName;
    return sInfo;
};

BaseNode.prototype.DBG = function( sMessage, nTime )
{
    ANIMDBG.print( sMessage, nTime );
};




function AnimationBaseNode( aAnimElem, aParentNode, aNodeContext )
{
    AnimationBaseNode.superclass.constructor.call( this, aAnimElem, aParentNode, aNodeContext );

    this.sClassName = 'AnimationBaseNode';
    this.bIsContainer = false;
    this.aTargetElement = null;
    this.bIsTargetTextElement = false;
    this.aAnimatedElement = null;
    this.aActivity = null;

    this.nMinFrameCount = undefined;
    this.eAdditiveMode = undefined;

}
extend( AnimationBaseNode, BaseNode );


AnimationBaseNode.prototype.parseElement = function()
{
    var bRet = AnimationBaseNode.superclass.parseElement.call( this );

    var aAnimElem = this.aElement;

    // targetElement attribute
    this.aTargetElement = null;
    var sTargetElementAttr = aAnimElem.getAttributeNS( NSS['smil'], 'targetElement' );
    if( sTargetElementAttr )
        this.aTargetElement = document.getElementById( sTargetElementAttr );

    if( !this.aTargetElement )
    {
        this.eCurrentState = INVALID_NODE;
        log( 'AnimationBaseNode.parseElement: target element not found: ' + sTargetElementAttr );
    }

    // sub-item attribute for text animated element
    var sSubItemAttr = aAnimElem.getAttributeNS( NSS['anim'], 'sub-item' );
    this.bIsTargetTextElement = ( sSubItemAttr && ( sSubItemAttr === 'text' ) );

    // additive attribute
    var sAdditiveAttr = aAnimElem.getAttributeNS( NSS['smil'], 'additive' );
    if( sAdditiveAttr && aAddittiveModeInMap[sAdditiveAttr] )
        this.eAdditiveMode = aAddittiveModeInMap[sAdditiveAttr];
    else
        this.eAdditiveMode = ADDITIVE_MODE_REPLACE;

    // set up min frame count value;
    this.nMinFrameCount = ( this.getDuration().isValue() )
            ? ( this.getDuration().getValue() * MINIMUM_FRAMES_PER_SECONDS )
            : MINIMUM_FRAMES_PER_SECONDS;
    if( this.nMinFrameCount < 1.0 )
        this.nMinFrameCount = 1;
    else if( this.nMinFrameCount > MINIMUM_FRAMES_PER_SECONDS )
        this.nMinFrameCount = MINIMUM_FRAMES_PER_SECONDS;


    if( this.aTargetElement )
    {
        // set up target element initial visibility
        if( aAnimElem.getAttributeNS( NSS['smil'], 'attributeName' ) === 'visibility' )
        {
            if( aAnimElem.getAttributeNS( NSS['smil'], 'to' ) === 'visible' )
                this.aTargetElement.setAttribute( 'visibility', 'hidden' );
        }

        // create animated element
        if( !this.aNodeContext.aAnimatedElementMap[ sTargetElementAttr ] )
        {
            if( this.bIsTargetTextElement )
            {
                this.aNodeContext.aAnimatedElementMap[ sTargetElementAttr ]
                    = new AnimatedTextElement( this.aTargetElement );
            }
            else
            {
                this.aNodeContext.aAnimatedElementMap[ sTargetElementAttr ]
                    = new AnimatedElement( this.aTargetElement );
            }
        }
        this.aAnimatedElement = this.aNodeContext.aAnimatedElementMap[ sTargetElementAttr ];

        // set additive mode
        this.aAnimatedElement.setAdditiveMode( this.eAdditiveMode );
    }


    return bRet;
};

AnimationBaseNode.prototype.init_st = function()
{
    if( this.aActivity )
        this.aActivity.activate( makeEvent( bind( this, this.deactivate ) ) );
    else
        this.aActivity = this.createActivity();
    return true;
};

AnimationBaseNode.prototype.resolve_st = function()
{
    return true;
};

AnimationBaseNode.prototype.activate_st = function()
{
    if( this.aActivity )
    {
        this.saveStateOfAnimatedElement();
        this.aActivity.setTargets( this.getAnimatedElement() );
        if( this.getContext().bIsSkipping  )
        {
            this.aActivity.end();
        }
        else
        {
            this.getContext().aActivityQueue.addActivity( this.aActivity );
        }
    }
    else
    {
        AnimationBaseNode.superclass.scheduleDeactivationEvent.call( this );
    }
};

AnimationBaseNode.prototype.deactivate_st = function( eDestState )
{
    if( eDestState == FROZEN_NODE )
    {
        if( this.aActivity )
            this.aActivity.end();
    }
    if( eDestState == ENDED_NODE )
    {
        if( this.aActivity )
            this.aActivity.dispose();
        if( ( this.getFillMode() == FILL_MODE_REMOVE ) && this.getAnimatedElement()  )
            this.removeEffect();
    }
};

AnimationBaseNode.prototype.createActivity = function()
{
    log( 'AnimationBaseNode.createActivity: abstract method called' );
};

AnimationBaseNode.prototype.fillActivityParams = function()
{

    // compute duration
    var nDuration = 0.001;
    if( this.getDuration().isValue() )
    {
        nDuration = this.getDuration().getValue();
    }
    else
    {
        log( 'AnimationBaseNode.fillActivityParams: duration is not a number' );
    }

    // create and set up activity params
    var aActivityParamSet = new ActivityParamSet();

    aActivityParamSet.aEndEvent             = makeEvent( bind( this, this.deactivate ) );
    aActivityParamSet.aTimerEventQueue      = this.aContext.aTimerEventQueue;
    aActivityParamSet.aActivityQueue        = this.aContext.aActivityQueue;
    aActivityParamSet.nMinDuration          = nDuration;
    aActivityParamSet.nMinNumberOfFrames    = this.getMinFrameCount();
    aActivityParamSet.bAutoReverse          = this.isAutoReverseEnabled();
    aActivityParamSet.nRepeatCount          = this.getRepeatCount();
    aActivityParamSet.nAccelerationFraction = this.getAccelerateValue();
    aActivityParamSet.nDecelerationFraction = this.getDecelerateValue();
    aActivityParamSet.nSlideWidth           = this.aNodeContext.aSlideWidth;
    aActivityParamSet.nSlideHeight          = this.aNodeContext.aSlideHeight;

    return aActivityParamSet;
};

AnimationBaseNode.prototype.hasPendingAnimation = function()
{
    return true;
};

AnimationBaseNode.prototype.saveStateOfAnimatedElement = function()
{
    this.getAnimatedElement().saveState( this.getId() );
};

AnimationBaseNode.prototype.removeEffect = function()
{
    this.getAnimatedElement().restoreState( this.getId() );
};

AnimationBaseNode.prototype.getTargetElement = function()
{
    return this.aTargetElement;
};

AnimationBaseNode.prototype.getAnimatedElement = function()
{
    return this.aAnimatedElement;
};

AnimationBaseNode.prototype.dispose= function()
{
    if( this.aActivity )
        this.aActivity.dispose();

    AnimationBaseNode.superclass.dispose.call( this );
};

AnimationBaseNode.prototype.getMinFrameCount = function()
{
    return this.nMinFrameCount;
};

AnimationBaseNode.prototype.getAdditiveMode = function()
{
    return this.eAdditiveMode;
};

AnimationBaseNode.prototype.info = function( bVerbose )
{
    var sInfo = AnimationBaseNode.superclass.info.call( this, bVerbose );

    if( bVerbose )
    {
        // min frame count
        if( this.getMinFrameCount() )
            sInfo += ';  min frame count: ' + this.getMinFrameCount();

        // additive mode
        sInfo += ';  additive: ' + aAddittiveModeOutMap[ this.getAdditiveMode() ];

        // target element
        if( this.getTargetElement() )
        {
            var sElemId = this.getTargetElement().getAttribute( 'id' );
            sInfo += ';  targetElement: ' +  sElemId;
        }
    }

    return sInfo;
};



function AnimationBaseNode2( aAnimElem, aParentNode, aNodeContext )
{
    AnimationBaseNode2.superclass.constructor.call( this, aAnimElem, aParentNode, aNodeContext );

    this.sAttributeName = '';
    this.aToValue = null;

}
extend( AnimationBaseNode2, AnimationBaseNode );


AnimationBaseNode2.prototype.parseElement = function()
{
    var bRet = AnimationBaseNode2.superclass.parseElement.call( this );

    var aAnimElem = this.aElement;

    // attributeName attribute
    this.sAttributeName = aAnimElem.getAttributeNS( NSS['smil'], 'attributeName' );
    if( !this.sAttributeName )
    {
        this.eCurrentState = INVALID_NODE;
        log( 'AnimationBaseNode2.parseElement: target attribute name not found: ' + this.sAttributeName );
    }

    // to attribute
    this.aToValue = aAnimElem.getAttributeNS( NSS['smil'], 'to' );

    return bRet;
};

AnimationBaseNode2.prototype.getAttributeName = function()
{
    return this.sAttributeName;
};

AnimationBaseNode2.prototype.getToValue = function()
{
    return this.aToValue;
};

AnimationBaseNode2.prototype.info = function( bVerbose )
{
    var sInfo = AnimationBaseNode2.superclass.info.call( this, bVerbose );

    if( bVerbose )
    {
        // attribute name
        if( this.getAttributeName() )
            sInfo += ';  attributeName: ' + this.getAttributeName();

        // To
        if( this.getToValue() )
            sInfo += ';  to: ' + this.getToValue();
    }

    return sInfo;
};




function AnimationBaseNode3( aAnimElem, aParentNode, aNodeContext )
{
    AnimationBaseNode3.superclass.constructor.call( this, aAnimElem, aParentNode, aNodeContext );

    this.eAccumulate = undefined;
    this.eCalcMode = undefined;
    this.aFromValue = null;
    this.aByValue = null;
    this.aKeyTimes = null;
    this.aValues = null;
    this.aFormula= null;
}
extend( AnimationBaseNode3, AnimationBaseNode2 );


AnimationBaseNode3.prototype.parseElement = function()
{
    var bRet = AnimationBaseNode3.superclass.parseElement.call( this );

    var aAnimElem = this.aElement;

    // accumulate attribute
    this.eAccumulate = ACCUMULATE_MODE_NONE;
    var sAccumulateAttr = aAnimElem.getAttributeNS( NSS['smil'], 'accumulate' );
    if( sAccumulateAttr == 'sum' )
        this.eAccumulate = ACCUMULATE_MODE_SUM;

    // calcMode attribute
    this.eCalcMode = CALC_MODE_LINEAR;
    var sCalcModeAttr = aAnimElem.getAttributeNS( NSS['smil'], 'calcMode' );
    if( sCalcModeAttr && aCalcModeInMap[ sCalcModeAttr ] )
        this.eCalcMode = aCalcModeInMap[ sCalcModeAttr ];

    // from attribute
    this.aFromValue = aAnimElem.getAttributeNS( NSS['smil'], 'from' );

    // by attribute
    this.aByValue = aAnimElem.getAttributeNS( NSS['smil'], 'by' );

    // keyTimes attribute
    this.aKeyTimes = [];
    var sKeyTimesAttr = aAnimElem.getAttributeNS( NSS['smil'], 'keyTimes' );
    sKeyTimesAttr = removeWhiteSpaces( sKeyTimesAttr );
    if( sKeyTimesAttr )
    {
        var aKeyTimes = sKeyTimesAttr.split( ';' );
        for( var i = 0; i < aKeyTimes.length; ++i )
            this.aKeyTimes.push( parseFloat( aKeyTimes[i] ) );
    }

    // values attribute
    var sValuesAttr = aAnimElem.getAttributeNS( NSS['smil'], 'values' );
    if( sValuesAttr )
    {
        this.aValues = sValuesAttr.split( ';' );
    }
    else
    {
        this.aValues = [];
    }

    // formula attribute
    this.aFormula = aAnimElem.getAttributeNS( NSS['anim'], 'formula' );

    return bRet;
};

AnimationBaseNode3.prototype.getAccumulate = function()
{
    return this.eAccumulate;
};

AnimationBaseNode3.prototype.getCalcMode = function()
{
    return this.eCalcMode;
};

AnimationBaseNode3.prototype.getFromValue = function()
{
    return this.aFromValue;
};

AnimationBaseNode3.prototype.getByValue = function()
{
    return this.aByValue;
};

AnimationBaseNode3.prototype.getKeyTimes = function()
{
    return this.aKeyTimes;
};

AnimationBaseNode3.prototype.getValues = function()
{
    return this.aValues;
};

AnimationBaseNode3.prototype.getFormula = function()
{
    return this.aFormula;
};

AnimationBaseNode3.prototype.info = function( bVerbose )
{
    var sInfo = AnimationBaseNode3.superclass.info.call( this, bVerbose );

    if( bVerbose )
    {
        // accumulate mode
        if( this.getAccumulate() )
            sInfo += ';  accumulate: ' + aAccumulateModeOutMap[ this.getAccumulate() ];

        // calcMode
        sInfo += ';  calcMode: ' + aCalcModeOutMap[ this.getCalcMode() ];

        // from
        if( this.getFromValue() )
            sInfo += ';  from: ' + this.getFromValue();

        // by
        if( this.getByValue() )
            sInfo += ';  by: ' + this.getByValue();

        // keyTimes
        if( this.getKeyTimes().length )
            sInfo += ';  keyTimes: ' + this.getKeyTimes().join( ',' );

        // values
        if( this.getValues().length )
            sInfo += ';  values: ' + this.getValues().join( ',' );

        // formula
        if( this.getFormula() )
            sInfo += ';  formula: ' + this.getFormula();
    }

    return sInfo;
};




function BaseContainerNode( aAnimElem, aParentNode, aNodeContext )
{
    BaseContainerNode.superclass.constructor.call( this, aAnimElem, aParentNode, aNodeContext );

    this.sClassName = 'BaseContainerNode';
    this.bIsContainer = true;
    this.aChildrenArray = [];
    this.nFinishedChildren = 0;
    this.bDurationIndefinite = false;
    this.nLeftIterations = 1;

    this.eImpressNodeType = undefined;
    this.ePresetClass =  undefined;
    this.ePresetId =  undefined;
}
extend( BaseContainerNode, BaseNode );


BaseContainerNode.prototype.parseElement= function()
{
    var bRet = BaseContainerNode.superclass.parseElement.call( this );

    var aAnimElem = this.aElement;

    // node-type attribute
    this.eImpressNodeType = IMPRESS_DEFAULT_NODE;
    var sNodeTypeAttr = aAnimElem.getAttributeNS( NSS['presentation'], 'node-type' );
    if( sNodeTypeAttr && aImpressNodeTypeInMap[ sNodeTypeAttr ] )
        this.eImpressNodeType = aImpressNodeTypeInMap[ sNodeTypeAttr ];
    this.bMainSequenceRootNode = ( this.eImpressNodeType == IMPRESS_MAIN_SEQUENCE_NODE );
    this.bInteractiveSequenceRootNode = ( this.eImpressNodeType == IMPRESS_INTERACTIVE_SEQUENCE_NODE );

    // preset-class attribute
    this.ePresetClass =  undefined;
    var sPresetClassAttr = aAnimElem.getAttributeNS( NSS['presentation'], 'preset-class' );
    if( sPresetClassAttr && aPresetClassInMap[ sPresetClassAttr ] )
        this.ePresetClass = aPresetClassInMap[ sPresetClassAttr ];

    // preset-id attribute
    this.ePresetId =  undefined;
    var sPresetIdAttr = aAnimElem.getAttributeNS( NSS['presentation'], 'preset-id' );
    if( sPresetIdAttr && aPresetIdInMap[ sPresetIdAttr ] )
        this.ePresetId = aPresetIdInMap[ sPresetIdAttr ];


    // parse children elements
    var nChildrenCount = this.aChildrenArray.length;
    for( var i = 0; i < nChildrenCount; ++i )
    {
        this.aChildrenArray[i].parseElement();
    }


    // resolve duration
    this.bDurationIndefinite
            = ( !this.getDuration() || this.getDuration().isIndefinite()  ) &&
              ( !this.getEnd() || ( this.getEnd().getType() != OFFSET_TIMING ) );

    return bRet;
};

BaseContainerNode.prototype.appendChildNode = function( aAnimationNode )
{
    if( ! this.checkValidNode() )
        return ;

    if( aAnimationNode.registerDeactivatingListener( this ) )
        this.aChildrenArray.push( aAnimationNode );
};

BaseContainerNode.prototype.removeAllChildrenNodes = function()
{
    this.aChildrenArray = [];
};

BaseContainerNode.prototype.init_st = function()
{
    this.nLeftIterations = this.getRepeatCount();

    return this.init_children();
};

BaseContainerNode.prototype.init_children = function()
{
    this.nFinishedChildren = 0;
    var nChildrenCount = this.aChildrenArray.length;
    var nInitChildren = 0;
    for( var i = 0; i < nChildrenCount; ++i )
    {
        if( this.aChildrenArray[i].init() )
        {
            ++nInitChildren;
        }
    }
    return ( nChildrenCount == nInitChildren );
};


BaseContainerNode.prototype.deactivate_st = function( eDestState )
{
    this.nLeftIterations = 0;
    if( eDestState == FROZEN_NODE )
    {
        // deactivate all children that are not FROZEN or ENDED:
        this.forEachChildNode( mem_fn( 'deactivate' ), ~( FROZEN_NODE | ENDED_NODE ) );
    }
    else
    {
        // end all children that are not ENDED:
        this.forEachChildNode( mem_fn( 'end' ), ~ENDED_NODE );
        if( this.getFillMode() == FILL_MODE_REMOVE )
            this.removeEffect();
    }
};

BaseContainerNode.prototype.hasPendingAnimation = function()
{
    var nChildrenCount = this.aChildrenArray.length;
    for( var i = 0; i < nChildrenCount; ++i )
    {
        if( this.aChildrenArray[i].hasPendingAnimation() )
            return true;
    }
    return false;
};

BaseContainerNode.prototype.activate_st = function()
{
    log( 'BaseContainerNode.activate_st: abstract method called' );
};

BaseContainerNode.prototype.notifyDeactivating = function( /*aAnimationNode*/ )
{
    log( 'BaseContainerNode.notifyDeactivating: abstract method called' );
};

BaseContainerNode.prototype.isDurationIndefinite = function()
{
    return this.bDurationIndefinite;
};

BaseContainerNode.prototype.isChildNode = function( aAnimationNode )
{
    var nChildrenCount = this.aChildrenArray.length;
    for( var i = 0; i < nChildrenCount; ++i )
    {
        if( this.aChildrenArray[i].getId() == aAnimationNode.getId() )
            return true;
    }
    return false;
};

BaseContainerNode.prototype.notifyDeactivatedChild = function( aChildNode )
{
    assert( ( aChildNode.getState() == FROZEN_NODE ) || ( aChildNode.getState() == ENDED_NODE ),
            'BaseContainerNode.notifyDeactivatedChild: passed child node is neither in FROZEN nor in ENDED state' );

    assert( this.getState() != INVALID_NODE,
            'BaseContainerNode.notifyDeactivatedChild: this node is invalid' );

    if( !this.isChildNode( aChildNode ) )
    {
        log( 'BaseContainerNode.notifyDeactivatedChild: unknown child notifier!' );
        return false;
    }

    var nChildrenCount = this.aChildrenArray.length;

    assert( ( this.nFinishedChildren < nChildrenCount ),
            'BaseContainerNode.notifyDeactivatedChild: assert(this.nFinishedChildren < nChildrenCount) failed' );

    ++this.nFinishedChildren;
    var bFinished = ( this.nFinishedChildren >= nChildrenCount );

    if( bFinished && this.isDurationIndefinite() )
    {
        if( this.nLeftIterations >= 1.0 )
        {
            this.nLeftIterations -= 1.0;
        }
        if( this.nLeftIterations >= 1.0 )
        {
            bFinished = false;
            var aRepetitionEvent = makeDelay( bind( this, this.repeat ), 0.0 );
            this.aContext.aTimerEventQueue.addEvent( aRepetitionEvent );
        }
        else
        {
            this.deactivate();
        }
    }

    return bFinished;
};

BaseContainerNode.prototype.repeat = function()
{
    // end all children that are not ENDED:
    this.forEachChildNode( mem_fn( 'end' ), ~ENDED_NODE );
    this.removeEffect();
    var bInitialized = this.init_children();
    if( bInitialized )
        this.activate_st();
    return bInitialized;
};

BaseContainerNode.prototype.removeEffect = function()
{
    var nChildrenCount = this.aChildrenArray.length;
    if( nChildrenCount == 0 )
        return;
    // We remove effect in reverse order.
    for( var i = nChildrenCount - 1; i >= 0; --i )
    {
        if( ( this.aChildrenArray[i].getState() & ( FROZEN_NODE | ENDED_NODE ) ) == 0 )
        {
            log( 'BaseContainerNode.removeEffect: child(id:'
                 + this.aChildrenArray[i].getId() + ') is neither frozen nor ended;'
                 + ' state: '
                 + aTransitionModeOutMap[ this.aChildrenArray[i].getState() ] );
            continue;
        }
        this.aChildrenArray[i].removeEffect();
    }
};

BaseContainerNode.prototype.saveStateOfAnimatedElement = function()
{
    var nChildrenCount = this.aChildrenArray.length;
    for( var i = 0; i < nChildrenCount; ++i )
    {
        this.aChildrenArray[i].saveStateOfAnimatedElement();
    }
};

BaseContainerNode.prototype.forEachChildNode = function( aFunction, eNodeStateMask )
{
    if( !eNodeStateMask )
        eNodeStateMask = -1;

    var nChildrenCount = this.aChildrenArray.length;
    for( var i = 0; i < nChildrenCount; ++i )
    {
        if( ( eNodeStateMask != -1 ) && ( ( this.aChildrenArray[i].getState() & eNodeStateMask ) == 0 ) )
            continue;
        aFunction( this.aChildrenArray[i] );
    }
};

BaseContainerNode.prototype.dispose = function()
{
    var nChildrenCount = this.aChildrenArray.length;
    for( var i = 0; i < nChildrenCount; ++i )
    {
        this.aChildrenArray[i].dispose();
    }

    BaseContainerNode.superclass.dispose.call( this );
};

BaseContainerNode.prototype.getImpressNodeType = function()
{
    return this.eImpressNodeType;
};

BaseContainerNode.prototype.info = function( bVerbose )
{
    var sInfo = BaseContainerNode.superclass.info.call( this, bVerbose );

    if( bVerbose )
    {
        // impress node type
        if( this.getImpressNodeType() )
            sInfo += ';  node-type: ' + aImpressNodeTypeOutMap[ this.getImpressNodeType() ];
    }

    var nChildrenCount = this.aChildrenArray.length;
    for( var i = 0; i < nChildrenCount; ++i )
    {
        sInfo += '\n';
        sInfo += this.aChildrenArray[i].info( bVerbose );
    }

    return sInfo;
};


function ParallelTimeContainer( aAnimElem, aParentNode, aNodeContext )
{
    ParallelTimeContainer.superclass.constructor.call( this, aAnimElem, aParentNode, aNodeContext );

    this.sClassName = 'ParallelTimeContainer';
}
extend( ParallelTimeContainer, BaseContainerNode );


ParallelTimeContainer.prototype.activate_st = function()
{
    var nChildrenCount = this.aChildrenArray.length;
    var nResolvedChildren = 0;
    for( var i = 0; i < nChildrenCount; ++i )
    {
        if( this.aChildrenArray[i].resolve() )
        {
            ++nResolvedChildren;
        }
    }

    if( nChildrenCount != nResolvedChildren )
    {
        log( 'ParallelTimeContainer.activate_st: resolving all children failed' );
        return;
    }


    if( this.isDurationIndefinite() && ( nChildrenCount == 0  ) )
    {
        this.scheduleDeactivationEvent( this.makeDeactivationEvent( 0.0 ) );
    }
    else
    {
        this.scheduleDeactivationEvent();
    }
};

ParallelTimeContainer.prototype.notifyDeactivating = function( aAnimationNode )
{
    this.notifyDeactivatedChild( aAnimationNode );
};




function SequentialTimeContainer( aAnimElem, aParentNode, aNodeContext )
{
    SequentialTimeContainer.superclass.constructor.call( this, aAnimElem, aParentNode, aNodeContext );

    this.sClassName = 'SequentialTimeContainer';
    this.bIsRewinding = false;
    this.aCurrentSkipEvent = null;
    this.aRewindCurrentEffectEvent = null;
    this.aRewindLastEffectEvent = null;
}
extend( SequentialTimeContainer, BaseContainerNode );


SequentialTimeContainer.prototype.activate_st = function()
{
    var nChildrenCount = this.aChildrenArray.length;
    for( ; this.nFinishedChildren < nChildrenCount; ++this.nFinishedChildren )
    {
        if( this.resolveChild( this.aChildrenArray[ this.nFinishedChildren ] ) )
            break;
        else
            log( 'SequentialTimeContainer.activate_st: resolving child failed!' );
    }

    if( this.isDurationIndefinite() && ( ( nChildrenCount == 0 ) || ( this.nFinishedChildren >= nChildrenCount ) ) )
    {
        // deactivate ASAP:
        this.scheduleDeactivationEvent( this.makeDeactivationEvent( 0.0 ) );
    }
    else
    {
        this.scheduleDeactivationEvent();
    }
};

SequentialTimeContainer.prototype.notifyDeactivating = function( aNotifier )
{
    // If we are rewinding we have not to resolve the next child.
    if( this.bIsRewinding )
        return;

    if( this.notifyDeactivatedChild( aNotifier ) )
        return;

    assert( this.nFinishedChildren < this.aChildrenArray.length,
            'SequentialTimeContainer.notifyDeactivating: assertion (this.nFinishedChildren < this.aChildrenArray.length) failed' );

    var aNextChild = this.aChildrenArray[ this.nFinishedChildren ];

    assert( aNextChild.getState() == UNRESOLVED_NODE,
            'SequentialTimeContainer.notifyDeactivating: assertion (aNextChild.getState == UNRESOLVED_NODE) failed' );

    if( !this.resolveChild( aNextChild ) )
    {
        // could not resolve child - since we risk to
        // stall the chain of events here, play it safe
        // and deactivate this node (only if we have
        // indefinite duration - otherwise, we'll get a
        // deactivation event, anyways).
        this.deactivate();
    }
};

/** skipEffect
 *  Skip the current playing shape effect.
 *  Requires: the current node is the main sequence root node.
 *
 *  @param aChildNode
 *      An animation node representing the root node of the shape effect being
 *      played.
 */
SequentialTimeContainer.prototype.skipEffect = function( aChildNode )
{
    if( this.isChildNode( aChildNode ) )
    {
        // First off we end all queued activities.
        this.getContext().aActivityQueue.endAll();
        // We signal that we are going to skip all subsequent animations by
        // setting the bIsSkipping flag to 'true', then all queued events are
        // fired immediately. In such a way the correct order of the various
        // events that belong to the animation time-line is preserved.
        this.getContext().bIsSkipping = true;
        this.getContext().aTimerEventQueue.forceEmpty();
        this.getContext().bIsSkipping = false;
        var aEvent = makeEvent( bind2( aChildNode.deactivate, aChildNode ) );
        this.getContext().aTimerEventQueue.addEvent( aEvent );
    }
    else
    {
        log( 'SequentialTimeContainer.skipEffect: unknown child: '
                 + aChildNode.getId() );
    }
};

/** rewindCurrentEffect
 *  Rewind a playing shape effect.
 *  Requires: the current node is the main sequence root node.
 *
 *  @param aChildNode
 *      An animation node representing the root node of the shape effect being
 *      played
 */
SequentialTimeContainer.prototype.rewindCurrentEffect = function( aChildNode )
{
    if( this.isChildNode( aChildNode ) )
    {
        assert( !this.bIsRewinding,
                'SequentialTimeContainer.rewindCurrentEffect: is already rewinding.' );

        // We signal we are rewinding so the notifyDeactivating method returns
        // immediately without increment the finished children counter and
        // resolve the next child.
        this.bIsRewinding = true;
        // First off we end all queued activities.
        this.getContext().aActivityQueue.endAll();
        // We signal that we are going to skip all subsequent animations by
        // setting the bIsSkipping flag to 'true', then all queued events are
        // fired immediately. In such a way the correct order of the various
        // events that belong to the animation time-line is preserved.
        this.getContext().bIsSkipping = true;
        this.getContext().aTimerEventQueue.forceEmpty();
        this.getContext().bIsSkipping = false;
        // We end all new activities appended to the activity queue by
        // the fired events.
        this.getContext().aActivityQueue.endAll();

        // Now we perform a final 'end' and restore the animated shape to
        // the state it was before the current effect was applied.
        aChildNode.end();
        aChildNode.removeEffect();
        // Finally we place the child node to the 'unresolved' state and
        // resolve it again.
        aChildNode.init();
        this.resolveChild( aChildNode );
        this.notifyRewindedEvent( aChildNode );
        this.bIsRewinding = false;
    }
    else
    {
        log( 'SequentialTimeContainer.rewindCurrentEffect: unknown child: '
                 + aChildNode.getId() );
    }
};

/** rewindLastEffect
 *  Rewind the last ended effect.
 *  Requires: the current node is the main sequence root node.
 *
 *  @param aChildNode
 *      An animation node representing the root node of the next shape effect
 *      to be played.
 */
SequentialTimeContainer.prototype.rewindLastEffect = function( aChildNode )
{
    if( this.isChildNode( aChildNode ) )
    {
        assert( !this.bIsRewinding,
                'SequentialTimeContainer.rewindLastEffect: is already rewinding.' );

        // We signal we are rewinding so the notifyDeactivating method returns
        // immediately without increment the finished children counter and
        // resolve the next child.
        this.bIsRewinding = true;
        // We end the current effect.
        this.getContext().aTimerEventQueue.forceEmpty();
        this.getContext().aActivityQueue.clear();
        aChildNode.end();
        // Invoking the end method on the current child node that has not yet
        // been activated should not lead to any change on the animated shape.
        // However for safety we used to call the removeEffect method but
        // lately we noticed that when interactive animation sequences are
        // involved into the shape effect invoking such a method causes
        // some issue.
        //aChildNode.removeEffect();

        // As we rewind the previous effect we need to decrease the finished
        // children counter.
        --this.nFinishedChildren;
        var aPreviousChildNode = this.aChildrenArray[ this.nFinishedChildren ];
        // No need to invoke the end method for the previous child as it is
        // already in the ENDED state.

        aPreviousChildNode.removeEffect();
        // We place the child node to the 'unresolved' state.
        aPreviousChildNode.init();
        // We need to re-initialize the old current child too, because it is
        // in ENDED state now, On the contrary it cannot be resolved again later.
        aChildNode.init();
        this.resolveChild( aPreviousChildNode );
        this.notifyRewindedEvent( aChildNode );
        this.bIsRewinding = false;
    }
    else
    {
        log( 'SequentialTimeContainer.rewindLastEffect: unknown child: '
                 + aChildNode.getId() );
    }
};

/** resolveChild
 *  Resolve the passed child.
 *  In case this node is a main sequence root node events for skipping and
 *  rewinding the effect related to the passed child node are created and
 *  registered.
 *
 *  @param aChildNode
 *      An animation node representing the root node of the next shape effect
 *      to be played.
 *  @return
 *      It returns true if the passed child has been resolved successfully,
 *      false otherwise.
 */
SequentialTimeContainer.prototype.resolveChild = function( aChildNode )
{
    var bResolved = aChildNode.resolve();

    if( bResolved && ( this.isMainSequenceRootNode() || this.isInteractiveSequenceRootNode() ) )
    {
        if( this.aCurrentSkipEvent )
            this.aCurrentSkipEvent.dispose();
        this.aCurrentSkipEvent = makeEvent( bind2( SequentialTimeContainer.prototype.skipEffect, this, aChildNode ) );

        if( this.aRewindCurrentEffectEvent )
            this.aRewindCurrentEffectEvent.dispose();
        this.aRewindCurrentEffectEvent = makeEvent( bind2( SequentialTimeContainer.prototype.rewindCurrentEffect, this, aChildNode ) );

        if( this.aRewindLastEffectEvent )
            this.aRewindLastEffectEvent.dispose();
        this.aRewindLastEffectEvent = makeEvent( bind2( SequentialTimeContainer.prototype.rewindLastEffect, this, aChildNode ) );

        if( this.isMainSequenceRootNode() )
        {
            this.aContext.aEventMultiplexer.registerSkipEffectEvent( this.aCurrentSkipEvent );
            this.aContext.aEventMultiplexer.registerRewindCurrentEffectEvent( this.aRewindCurrentEffectEvent );
            this.aContext.aEventMultiplexer.registerRewindLastEffectEvent( this.aRewindLastEffectEvent );
        }
        else if( this.isInteractiveSequenceRootNode() )
        {
            this.aContext.aEventMultiplexer.registerSkipInteractiveEffectEvent( aChildNode.getId(), this.aCurrentSkipEvent );
            this.aContext.aEventMultiplexer.registerRewindRunningInteractiveEffectEvent( aChildNode.getId(), this.aRewindCurrentEffectEvent );
            this.aContext.aEventMultiplexer.registerRewindEndedInteractiveEffectEvent( aChildNode.getId(), this.aRewindLastEffectEvent );
        }
    }
    return bResolved;
};

SequentialTimeContainer.prototype.notifyRewindedEvent = function( aChildNode )
{
    if( this.isInteractiveSequenceRootNode() )
    {
        this.aContext.aEventMultiplexer.notifyRewindedEffectEvent( aChildNode.getId() );

        var sId = aChildNode.getBegin().getEventBaseElementId();
        if( sId )
        {
            this.aContext.aEventMultiplexer.notifyRewindedEffectEvent( sId );
        }
    }
};

SequentialTimeContainer.prototype.dispose = function()
{
    if( this.aCurrentSkipEvent )
        this.aCurrentSkipEvent.dispose();

    SequentialTimeContainer.superclass.dispose.call( this );
};




function PropertyAnimationNode(  aAnimElem, aParentNode, aNodeContext )
{
    PropertyAnimationNode.superclass.constructor.call( this, aAnimElem, aParentNode, aNodeContext );

    this.sClassName = 'PropertyAnimationNode';
}
extend( PropertyAnimationNode, AnimationBaseNode3 );


PropertyAnimationNode.prototype.createActivity = function()
{
    var aActivityParamSet = this.fillActivityParams();

    var aAnimation = createPropertyAnimation( this.getAttributeName(),
                                              this.getAnimatedElement(),
                                              this.aNodeContext.aSlideWidth,
                                              this.aNodeContext.aSlideHeight );

    var aInterpolator = null;  // createActivity will compute it;
    return createActivity( aActivityParamSet, this, aAnimation, aInterpolator );
};




function isValidTransformation( sType )
{
    return ( sType === 'translate' || sType === 'scale' || sType === 'rotate'
          || sType === 'skewX' || sType === 'skewY' );
}

function AnimationTransformNode(  aAnimElem, aParentNode, aNodeContext )
{
    AnimationTransformNode.superclass.constructor.call( this, aAnimElem, aParentNode, aNodeContext );

    this.sClassName = 'AnimationTransformNode';
}
extend( AnimationTransformNode, AnimationBaseNode3 );


AnimationTransformNode.prototype.parseElement = function()
{
    var bRet = AnimationTransformNode.superclass.parseElement.call(this);

    var aAnimElem = this.aElement;

    // transformation type
    var sTransformType = aAnimElem.getAttribute( 'svg:type' );
    if( !isValidTransformation( sTransformType ) )
    {
        this.eCurrentState = INVALID_NODE;
        log( 'AnimationTransformNode.parseElement: transformation type not found: ' + sTransformType );
    }
    else
    {
        this.sAttributeName = sTransformType;
    }

    return bRet;
}

AnimationTransformNode.prototype.createActivity = function()
{
    var aActivityParamSet = this.fillActivityParams();
    var aAnimation;

    if( this.getAttributeName() === 'scale' || this.getAttributeName() === 'translate' )
    {
        aAnimation = createPairPropertyAnimation( this.getAttributeName(),
                                                  this.getAnimatedElement(),
                                                  this.aNodeContext.aSlideWidth,
                                                  this.aNodeContext.aSlideHeight );

    }
    else
    {
        aAnimation = createPropertyAnimation( this.getAttributeName(),
                                              this.getAnimatedElement(),
                                              this.aNodeContext.aSlideWidth,
                                              this.aNodeContext.aSlideHeight );
    }

    var aInterpolator = null;  // createActivity will compute it;
    return createActivity( aActivityParamSet, this, aAnimation, aInterpolator );
};




function AnimationSetNode(  aAnimElem, aParentNode, aNodeContext )
{
    AnimationSetNode.superclass.constructor.call( this, aAnimElem, aParentNode, aNodeContext );

    this.sClassName = 'AnimationSetNode';
}
extend( AnimationSetNode, AnimationBaseNode2 );


AnimationSetNode.prototype.createActivity = function()
{
    var aAnimation = createPropertyAnimation( this.getAttributeName(),
                                              this.getAnimatedElement(),
                                              this.aNodeContext.aSlideWidth,
                                              this.aNodeContext.aSlideHeight );

    var aActivityParamSet = this.fillActivityParams();

    return new SetActivity( aActivityParamSet, aAnimation, this.getToValue() );
};




function AnimationColorNode(  aAnimElem, aParentNode, aNodeContext )
{
    AnimationColorNode.superclass.constructor.call( this, aAnimElem, aParentNode, aNodeContext );

    this.sClassName = 'AnimationColorNode';

    this.eColorInterpolation = undefined;
    this.eColorInterpolationDirection = undefined;
}
extend( AnimationColorNode, AnimationBaseNode3 );


AnimationColorNode.prototype.parseElement = function()
{
    var bRet = AnimationColorNode.superclass.parseElement.call( this );

    var aAnimElem = this.aElement;

    // color-interpolation attribute
    this.eColorInterpolation = COLOR_SPACE_RGB;
    var sColorInterpolationAttr = aAnimElem.getAttributeNS( NSS['anim'], 'color-interpolation' );
    if( sColorInterpolationAttr && aColorSpaceInMap[ sColorInterpolationAttr ] )
        this.eColorInterpolation = aColorSpaceInMap[ sColorInterpolationAttr ];

    // color-interpolation-direction attribute
    this.eColorInterpolationDirection = CLOCKWISE;
    var sColorInterpolationDirectionAttr = aAnimElem.getAttributeNS( NSS['anim'], 'color-interpolation-direction' );
    if( sColorInterpolationDirectionAttr && aClockDirectionInMap[ sColorInterpolationDirectionAttr ] )
        this.eColorInterpolationDirection = aClockDirectionInMap[ sColorInterpolationDirectionAttr ];

    return bRet;
};

AnimationColorNode.prototype.createActivity = function()
{
    var aActivityParamSet = this.fillActivityParams();

    var aAnimation = createPropertyAnimation( this.getAttributeName(),
                                              this.getAnimatedElement(),
                                              this.aNodeContext.aSlideWidth,
                                              this.aNodeContext.aSlideHeight );

    var aColorAnimation;
    var aInterpolator;
    if( this.getColorInterpolation() === COLOR_SPACE_HSL )
    {
        ANIMDBG.print( 'AnimationColorNode.createActivity: color space hsl'  );
        aColorAnimation = new HSLAnimationWrapper( aAnimation );
        var aInterpolatorMaker = aInterpolatorHandler.getInterpolator( this.getCalcMode(),
                                                                       COLOR_PROPERTY,
                                                                       COLOR_SPACE_HSL );
        aInterpolator = aInterpolatorMaker( this.getColorInterpolationDirection() );
    }
    else
    {
        ANIMDBG.print( 'AnimationColorNode.createActivity: color space rgb'  );
        aColorAnimation = aAnimation;
        aInterpolator = aInterpolatorHandler.getInterpolator( this.getCalcMode(),
                                                              COLOR_PROPERTY,
                                                              COLOR_SPACE_RGB );
    }

    return createActivity( aActivityParamSet, this, aColorAnimation, aInterpolator );
};

AnimationColorNode.prototype.getColorInterpolation = function()
{
    return this.eColorInterpolation;
};

AnimationColorNode.prototype.getColorInterpolationDirection = function()
{
    return this.eColorInterpolationDirection;
};

AnimationColorNode.prototype.info = function( bVerbose )
{
    var sInfo = AnimationColorNode.superclass.info.call( this, bVerbose );

    if( bVerbose )
    {
        // color interpolation
        sInfo += ';  color-interpolation: ' + aColorSpaceOutMap[ this.getColorInterpolation() ];

        // color interpolation direction
        sInfo += ';  color-interpolation-direction: ' + aClockDirectionOutMap[ this.getColorInterpolationDirection() ];
    }
    return sInfo;
};




function AnimationTransitionFilterNode(  aAnimElem, aParentNode, aNodeContext )
{
    AnimationTransitionFilterNode.superclass.constructor.call( this, aAnimElem, aParentNode, aNodeContext );

    this.sClassName = 'AnimationTransitionFilterNode';

    this.eTransitionType = undefined;
    this.eTransitionSubType = undefined;
    this.bReverseDirection = undefined;
    this.eTransitionMode = undefined;
}
extend( AnimationTransitionFilterNode, AnimationBaseNode );


AnimationTransitionFilterNode.prototype.createActivity = function()
{
    var aActivityParamSet = this.fillActivityParams();

    return createShapeTransition( aActivityParamSet,
                                  this.getAnimatedElement(),
                                  this.aNodeContext.aSlideWidth,
                                  this.aNodeContext.aSlideHeight,
                                  this );
};

AnimationTransitionFilterNode.prototype.parseElement = function()
{
    var bRet = AnimationTransitionFilterNode.superclass.parseElement.call( this );
    var bIsValidTransition = true;

    var aAnimElem = this.aElement;

    // type attribute
    this.eTransitionType = undefined;
    var sTypeAttr = aAnimElem.getAttributeNS( NSS['smil'], 'type' );
    if( sTypeAttr && aTransitionTypeInMap[ sTypeAttr ] )
    {
        this.eTransitionType = aTransitionTypeInMap[ sTypeAttr ];
    }
    else
    {
        bIsValidTransition = false;
        log( 'AnimationTransitionFilterNode.parseElement: transition type not valid: ' + sTypeAttr );
    }

    // subtype attribute
    this.eTransitionSubType = undefined;
    var sSubTypeAttr = aAnimElem.getAttributeNS( NSS['smil'], 'subtype' );
    if( sSubTypeAttr === null )
        sSubTypeAttr = 'default';
    if( sSubTypeAttr && ( aTransitionSubtypeInMap[ sSubTypeAttr ] !== undefined  ) )
    {
        this.eTransitionSubType = aTransitionSubtypeInMap[ sSubTypeAttr ];
    }
    else
    {
        bIsValidTransition = false;
        log( 'AnimationTransitionFilterNode.parseElement: transition subtype not valid: ' + sSubTypeAttr );
    }

    // if we do not support the requested transition type we fall back to crossfade transition;
    // note: if we do not provide an alternative transition and we set the state of the animation node to 'invalid'
    // the animation engine stops itself;
    if( !bIsValidTransition )
    {
        this.eTransitionType = FADE_TRANSITION;
        this.eTransitionSubType = CROSSFADE_TRANS_SUBTYPE;
        log( 'AnimationTransitionFilterNode.parseElement: in place of the invalid transition a crossfade transition is used' );
    }

    // direction attribute
    this.bReverseDirection = false;
    var sDirectionAttr = aAnimElem.getAttributeNS( NSS['smil'], 'direction' );
    if( sDirectionAttr == 'reverse' )
        this.bReverseDirection = true;

    // mode attribute:
    this.eTransitionMode = TRANSITION_MODE_IN;
    var sModeAttr = aAnimElem.getAttributeNS( NSS['smil'], 'mode' );
    if( sModeAttr === 'out' )
        this.eTransitionMode = TRANSITION_MODE_OUT;

    return bRet;
};

AnimationTransitionFilterNode.prototype.getTransitionType = function()
{
    return this.eTransitionType;
};

AnimationTransitionFilterNode.prototype.getTransitionSubType = function()
{
    return this.eTransitionSubType;
};

AnimationTransitionFilterNode.prototype.getTransitionMode = function()
{
    return this.eTransitionMode;
};

AnimationTransitionFilterNode.prototype.getReverseDirection = function()
{
    return this.bReverseDirection;
};

AnimationTransitionFilterNode.prototype.info = function( bVerbose )
{
    var sInfo = AnimationTransitionFilterNode.superclass.info.call( this, bVerbose );

    if( bVerbose )
    {
        // transition type
        sInfo += ';  type: ' + getKeyByValue(aTransitionTypeInMap, this.getTransitionType());

        // transition subtype
        sInfo += ';  subtype: ' + getKeyByValue(aTransitionSubtypeInMap, this.getTransitionSubType());

        // transition direction
        if( this.getReverseDirection() )
            sInfo += ';  direction: reverse';
    }

    return sInfo;
};



/**********************************************************************************************
 *      Animation Node Factory
 **********************************************************************************************/


function createAnimationTree( aRootElement, aNodeContext )
{
    return createAnimationNode( aRootElement, null, aNodeContext );
}




function createAnimationNode( aElement, aParentNode, aNodeContext )
{
    assert( aElement, 'createAnimationNode: invalid animation element' );

    var eAnimationNodeType = getAnimationElementType( aElement );

    var aCreatedNode = null;
    var aCreatedContainer = null;

    switch( eAnimationNodeType )
    {
        case ANIMATION_NODE_PAR:
            aCreatedNode = aCreatedContainer =
                new ParallelTimeContainer( aElement, aParentNode, aNodeContext );
            break;
        case ANIMATION_NODE_ITERATE:
            // map iterate container to ParallelTimeContainer.
            // the iterating functionality is to be found
            // below, (see method implCreateIteratedNodes)
            aCreatedNode = aCreatedContainer =
                new ParallelTimeContainer( aElement, aParentNode, aNodeContext );
            break;
        case ANIMATION_NODE_SEQ:
            aCreatedNode = aCreatedContainer =
                new SequentialTimeContainer( aElement, aParentNode, aNodeContext );
            break;
        case ANIMATION_NODE_ANIMATE:
            aCreatedNode = new PropertyAnimationNode( aElement, aParentNode, aNodeContext );
            break;
        case ANIMATION_NODE_SET:
            aCreatedNode = new AnimationSetNode( aElement, aParentNode, aNodeContext );
            break;
        case ANIMATION_NODE_ANIMATEMOTION:
            //aCreatedNode = new AnimationPathMotionNode( aElement, aParentNode, aNodeContext );
            //break;
            log( 'createAnimationNode: ANIMATEMOTION not implemented' );
            return null;
        case ANIMATION_NODE_ANIMATECOLOR:
            aCreatedNode = new AnimationColorNode( aElement, aParentNode, aNodeContext );
            break;
        case ANIMATION_NODE_ANIMATETRANSFORM:
            aCreatedNode = new AnimationTransformNode( aElement, aParentNode, aNodeContext );
            break;
        case ANIMATION_NODE_TRANSITIONFILTER:
            aCreatedNode = new AnimationTransitionFilterNode( aElement, aParentNode, aNodeContext );
            break;
         case ANIMATION_NODE_AUDIO:
            log( 'createAnimationNode: AUDIO not implemented' );
            return null;
         case ANIMATION_NODE_COMMAND:
            log( 'createAnimationNode: COMMAND not implemented' );
            return null;
        default:
            log( 'createAnimationNode: invalid Animation Node Type: ' + eAnimationNodeType );
            return null;
    }

    if( aCreatedContainer )
    {
        if( eAnimationNodeType == ANIMATION_NODE_ITERATE )
        {
            createIteratedNodes( aElement, aCreatedContainer, aNodeContext );
        }
        else
        {
            var aChildrenArray = getElementChildren( aElement );
            for( var i = 0; i < aChildrenArray.length; ++i )
            {
                if( !createChildNode( aChildrenArray[i], aCreatedContainer, aNodeContext ) )
                {
                    aCreatedContainer.removeAllChildrenNodes();
                    break;
                }
            }
        }
    }

    return aCreatedNode;
}




function createChildNode( aElement, aParentNode, aNodeContext )
{
    var aChildNode = createAnimationNode( aElement, aParentNode, aNodeContext );

    if( !aChildNode )
    {
        log( 'createChildNode: child node creation failed' );
        return false;
    }
    else
    {
        aParentNode.appendChildNode( aChildNode );
        return true;
    }
}




function createIteratedNodes( /*aElement, aContainerNode, aNodeContext*/ )
{
    // not implemented
}



/**********************************************************************************************
 *      Animation Factory
 **********************************************************************************************/

// makeScaler is used in aAttributeMap:
// eslint-disable-next-line no-unused-vars
function makeScaler( nScale )
{
    if( ( typeof( nScale ) !== typeof( 0 ) ) || !isFinite( nScale ) )
    {
        log( 'makeScaler: not valid param passed: ' + nScale );
        return null;
    }

    return  function( nValue )
            {
                return ( nScale * nValue );
            };
}



// eslint-disable-next-line no-unused-vars
function createPropertyAnimation( sAttrName, aAnimatedElement, nWidth, nHeight )
{
    if( !aAttributeMap[ sAttrName ] )
    {
        log( 'createPropertyAnimation: attribute is unknown' );
        return null;
    }


    var aFunctorSet = aAttributeMap[ sAttrName ];

    var sGetValueMethod =   aFunctorSet.get;
    var sSetValueMethod =   aFunctorSet.set;

    if( !sGetValueMethod || !sSetValueMethod  )
    {
        log( 'createPropertyAnimation: attribute is not handled' );
        return null;
    }

    var aGetModifier =  eval( aFunctorSet.getmod );
    var aSetModifier =  eval( aFunctorSet.setmod );


    return new GenericAnimation( bind( aAnimatedElement, aAnimatedElement[ sGetValueMethod ] ),
                                 bind( aAnimatedElement, aAnimatedElement[ sSetValueMethod ] ),
                                 aGetModifier,
                                 aSetModifier);
}




function createPairPropertyAnimation( sTransformType, aAnimatedElement, nWidth, nHeight )
{
    var aFunctorSet = aAttributeMap[ sTransformType ];
    var sGetValueMethod = aFunctorSet.get;
    var sSetValueMethod = aFunctorSet.set;

    var aDefaultValue = [];
    var aSizeReference = [];
    if( sTransformType === 'scale' )
    {
        aDefaultValue[0] = aSizeReference[0] = aAnimatedElement.getBaseBBox().width;
        aDefaultValue[1] = aSizeReference[1] = aAnimatedElement.getBaseBBox().height;
    }
    else if( sTransformType === 'translate' )
    {
        aDefaultValue[0] = aAnimatedElement.getBaseCenterX();
        aDefaultValue[1] = aAnimatedElement.getBaseCenterY();
        aSizeReference[0] = nWidth;
        aSizeReference[1] = nHeight;
    }
    else
    {
        log( 'createPairPropertyAnimation: transform type is not handled' );
        return null;
    }

    return new TupleAnimation( bind( aAnimatedElement, aAnimatedElement[ sGetValueMethod ] ),
                               bind( aAnimatedElement, aAnimatedElement[ sSetValueMethod ] ),
                               aDefaultValue,
                               aSizeReference );
}




/** createShapeTransition
 *
 *  @param aActivityParamSet
 *     The set of property for the activity to be created.
 *  @param aAnimatedElement
 *      The element to be animated.
 *  @param nSlideWidth
 *      The width of a slide.
 *  @param nSlideHeight
 *      The height of a slide.
 *  @param aAnimatedTransitionFilterNode
 *      An instance of the AnimationFilterNode that invoked this function.
 *  @return {SimpleActivity}
 *      A simple activity handling a shape transition.
 */
function createShapeTransition( aActivityParamSet, aAnimatedElement,
                                nSlideWidth, nSlideHeight,
                                aAnimatedTransitionFilterNode )
{
    if( !aAnimatedTransitionFilterNode )
    {
        log( 'createShapeTransition: the animated transition filter node is not valid.' );
        return null;
    }
    var eTransitionType = aAnimatedTransitionFilterNode.getTransitionType();
    var eTransitionSubType = aAnimatedTransitionFilterNode.getTransitionSubType();
    var bDirectionForward = ! aAnimatedTransitionFilterNode.getReverseDirection();
    var bModeIn = ( aAnimatedTransitionFilterNode.getTransitionMode() == FORWARD );

    var aTransitionInfo = aTransitionInfoTable[eTransitionType][eTransitionSubType];
    var eTransitionClass = aTransitionInfo['class'];

    switch( eTransitionClass )
    {
        default:
        case TRANSITION_INVALID:
            log( 'createShapeTransition: transition class: TRANSITION_INVALID' );
            return null;

        case TRANSITION_CLIP_POLYPOLYGON:
            var aParametricPolyPolygon
                = createClipPolyPolygon( eTransitionType, eTransitionSubType );
            var aClippingAnimation
                = new ClippingAnimation( aParametricPolyPolygon, aTransitionInfo,
                                         bDirectionForward, bModeIn );
            return new SimpleActivity( aActivityParamSet, aClippingAnimation, true );

        case TRANSITION_SPECIAL:
            switch( eTransitionType )
            {
                // no special transition filter provided
                // we map everything to crossfade
                default:
                    var aAnimation
                        = createPropertyAnimation( 'opacity',
                                                   aAnimatedElement,
                                                   nSlideWidth,
                                                   nSlideHeight );
                    return new SimpleActivity( aActivityParamSet, aAnimation, bModeIn );
            }
    }

}




/** Class ClippingAnimation
 *  This class performs a shape transition where the effect is achieved by
 *  clipping the shape to be animated with a parametric path.
 *
 *  @param aParametricPolyPolygon
 *      An object handling a <path> element that depends on a parameter.
 *  @param aTransitionInfo
 *      The set of parameters defining the shape transition to be performed.
 *  @param bDirectionForward
 *      The direction the shape transition has to be performed.
 *  @param bModeIn
 *      If true the element to be animated becomes more visible as the transition
 *      progress else it becomes less visible.
 */
function ClippingAnimation( aParametricPolyPolygon, aTransitionInfo,
                            bDirectionForward, bModeIn )
{
    this.aClippingFunctor = new ClippingFunctor( aParametricPolyPolygon,
                                                 aTransitionInfo,
                                                 bDirectionForward, bModeIn );
    this.bAnimationStarted = false;
}

/** start
 *  This method notifies to the element involved in the transition that
 *  the animation is starting and creates the <clipPath> element used for
 *  the transition.
 *
 *  @param aAnimatableElement
 *      The element to be animated.
 */
ClippingAnimation.prototype.start = function( aAnimatableElement )
{
    assert( aAnimatableElement,
            'ClippingAnimation.start: animatable element is not valid' );
    this.aAnimatableElement = aAnimatableElement;
    this.aAnimatableElement.initClipPath();
    this.aAnimatableElement.notifyAnimationStart();

    if( !this.bAnimationStarted )
        this.bAnimationStarted = true;

};

/** end
 *  The transition clean up is performed here.
 */
ClippingAnimation.prototype.end = function()
{
    if( this.bAnimationStarted )
    {
        this.aAnimatableElement.cleanClipPath();
        this.bAnimationStarted = false;
        this.aAnimatableElement.notifyAnimationEnd();
    }
};

/** perform
 *  This method set the position of the element to be animated according to
 *  the passed  time value.
 *
 *  @param nValue
 *      The time parameter.
 */
ClippingAnimation.prototype.perform = function( nValue )
{
    var nWidth = this.aAnimatableElement.aClippingBBox.width;
    var nHeight = this.aAnimatableElement.aClippingBBox.height;
    var aPolyPolygonElement = this.aClippingFunctor.perform( nValue, nWidth, nHeight );
    this.aAnimatableElement.setClipPath( aPolyPolygonElement );
};

ClippingAnimation.prototype.getUnderlyingValue = function()
{
    return 0.0;
};




function GenericAnimation( aGetValueFunc, aSetValueFunc, aGetModifier, aSetModifier )
{
    assert( aGetValueFunc && aSetValueFunc,
            'GenericAnimation constructor: get value functor and/or set value functor are not valid' );

    this.aGetValueFunc = aGetValueFunc;
    this.aSetValueFunc = aSetValueFunc;
    this.aGetModifier = aGetModifier;
    this.aSetModifier = aSetModifier;
    this.aAnimatableElement = null;
    this.bAnimationStarted = false;
}


GenericAnimation.prototype.start = function( aAnimatableElement )
{
    assert( aAnimatableElement, 'GenericAnimation.start: animatable element is not valid' );

    this.aAnimatableElement = aAnimatableElement;
    this.aAnimatableElement.notifyAnimationStart();

    if( !this.bAnimationStarted )
        this.bAnimationStarted = true;
};

GenericAnimation.prototype.end = function()
{
    if( this.bAnimationStarted )
    {
        this.bAnimationStarted = false;
        this.aAnimatableElement.notifyAnimationEnd();
    }
};

GenericAnimation.prototype.perform = function( aValue )
{
    if( this.aSetModifier )
        aValue = this.aSetModifier( aValue );

    this.aSetValueFunc( aValue );
};

GenericAnimation.prototype.getUnderlyingValue = function()
{
    var aValue = this.aGetValueFunc();
    if( this.aGetModifier )
        aValue = this.aGetModifier( aValue );
    return aValue;
};



function TupleAnimation( aGetValueFunc, aSetValueFunc, aDefaultValue, aReferenceSize )
{
    TupleAnimation.superclass.constructor.call( this, aGetValueFunc, aSetValueFunc );
    assert( aDefaultValue && aReferenceSize,
            'TupleAnimation constructor: default value functor and/or reference size are not valid' );

    this.aDefaultValue = aDefaultValue;
    this.aReferenceSize = aReferenceSize;
}
extend( TupleAnimation, GenericAnimation );

TupleAnimation.prototype.perform = function( aNormValue )
{
    assert(aNormValue.length === this.aReferenceSize.length);

    var aValue = [];
    for( var i = 0; i < aNormValue.length; ++i )
    {
        aValue.push( aNormValue[i] * this.aReferenceSize[i] );
    }

    this.aSetValueFunc( aValue );
};

TupleAnimation.prototype.getUnderlyingValue = function()
{
    var aValue = this.aGetValueFunc();
    assert(aValue.length === this.aReferenceSize.length);

    var aNormValue = [];
    for( var i = 0; i < aValue.length; ++i )
    {
        aNormValue.push( aValue[i] / this.aReferenceSize[i] );
    }

    return aNormValue;
};



function HSLAnimationWrapper( aColorAnimation )
{
    assert( aColorAnimation,
            'HSLAnimationWrapper constructor: invalid color animation delegate' );

    this.aAnimation = aColorAnimation;
}


HSLAnimationWrapper.prototype.start = function( aAnimatableElement )
{
    this.aAnimation.start( aAnimatableElement );
};

HSLAnimationWrapper.prototype.end = function()
{
    this.aAnimation.end();
};
HSLAnimationWrapper.prototype.perform = function( aHSLValue )
{
    this.aAnimation.perform( aHSLValue.convertToRGB() );
};

HSLAnimationWrapper.prototype.getUnderlyingValue = function()
{
    return this.aAnimation.getUnderlyingValue().convertToHSL();
};




/** Class SlideChangeBase
 *  The base abstract class of classes performing slide transitions.
 *
 *  @param aLeavingSlide
 *      An object of type AnimatedSlide handling the leaving slide.
 *  @param aEnteringSlide
 *      An object of type AnimatedSlide handling the entering slide.
 */
function SlideChangeBase(aLeavingSlide, aEnteringSlide)
{
    this.aLeavingSlide = aLeavingSlide;
    this.aEnteringSlide = aEnteringSlide;
    this.bIsFinished = false;
}

/** start
 *  The transition initialization is performed here.
 */
SlideChangeBase.prototype.start = function()
{
};

/** end
 *  The transition clean up is performed here.
 */
SlideChangeBase.prototype.end = function()
{
    if( this.bIsFinished )
        return;

    this.aLeavingSlide.hide();
    this.aEnteringSlide.reset();
    this.aLeavingSlide.reset();

    this.bIsFinished = true;
};

/** perform
 *  This method is responsible for performing the slide transition.
 *
 *  @param nValue
 *      The time parameter.
 *  @return {Boolean}
 *      If the transition is performed returns tue else returns false.
 */
SlideChangeBase.prototype.perform = function( nValue )
{
    if( this.bIsFinished ) return false;

    if( this.aLeavingSlide )
        this.performOut( nValue );

    if( this.aEnteringSlide )
        this.performIn( nValue );

    return true;
};

SlideChangeBase.prototype.getUnderlyingValue = function()
{
    return 0.0;
};

SlideChangeBase.prototype.performIn = function( )
{
    log( 'SlideChangeBase.performIn: abstract method called' );
};

SlideChangeBase.prototype.performOut = function( )
{
    log( 'SlideChangeBase.performOut: abstract method called' );
};




/** Class FadingSlideChange
 *  This class performs a slide transition by fading out the leaving slide and
 *  fading in the entering slide.
 *
 *  @param aLeavingSlide
 *      An object of type AnimatedSlide handling the leaving slide.
 *  @param aEnteringSlide
 *      An object of type AnimatedSlide handling the entering slide.
 */
function FadingSlideChange( aLeavingSlide, aEnteringSlide )
{
    FadingSlideChange.superclass.constructor.call( this, aLeavingSlide, aEnteringSlide );
    this.bFirstRun = true;
}
extend( FadingSlideChange, SlideChangeBase );

/** start
 *  This method notifies to the slides involved in the transition the attributes
 *  appended to the slide elements for performing the animation.
 *  Moreover it sets the entering slide in the initial state and makes the slide
 *  visible.
 */
FadingSlideChange.prototype.start = function()
{
    FadingSlideChange.superclass.start.call( this );
    this.aEnteringSlide.notifyUsedAttribute( 'opacity' );
    this.aLeavingSlide.notifyUsedAttribute( 'opacity' );
    this.aEnteringSlide.setOpacity( 0.0 );
    this.aEnteringSlide.show();
};

/** performIn
 *  This method set the opacity of the entering slide according to the passed
 *  time value.
 *
 *  @param nT
 *      The time parameter.
 */
FadingSlideChange.prototype.performIn = function( nT )
{
    this.aEnteringSlide.setOpacity( nT );
};

/** performOut
 *  This method set the opacity of the leaving slide according to the passed
 *  time value.
 *
 *  @param nT
 *      The time parameter.
 */
FadingSlideChange.prototype.performOut = function( nT )
{

    this.aLeavingSlide.setOpacity( 1 - nT );
};




/** Class FadingOverColorSlideChange
 *  This class performs a slide transition by fading out the leaving slide to
 *  a given color and fading in the entering slide from the same color.
 *
 *  @param aLeavingSlide
 *      An object of type AnimatedSlide handling the leaving slide.
 *  @param aEnteringSlide
 *      An object of type AnimatedSlide handling the entering slide.
 *  @param sFadeColor
 *      A string representing the color the leaving slide fades out to and
 *      the entering slide fade in from.
 */
function FadingOverColorSlideChange( aLeavingSlide, aEnteringSlide, sFadeColor )
{
    FadingSlideChange.superclass.constructor.call( this, aLeavingSlide, aEnteringSlide );
    this.sFadeColor = sFadeColor;
    if( !this.sFadeColor )
    {
        log( 'FadingOverColorSlideChange: sFadeColor not valid.' );
        this.sFadeColor = '#000000';
    }
    this.aColorPlaneElement = this.createColorPlaneElement();
}
extend( FadingOverColorSlideChange, SlideChangeBase );

/** start
 *  This method notifies to the slides involved in the transition the attributes
 *  appended to the slide elements for performing the animation.
 *  Moreover it inserts the color plane element below the leaving slide.
 *  Finally it sets the entering slide in the initial state and makes
 *  the slide visible.
 */
FadingOverColorSlideChange.prototype.start = function()
{
    FadingOverColorSlideChange.superclass.start.call( this );
    this.aEnteringSlide.notifyUsedAttribute( 'opacity' );
    this.aLeavingSlide.notifyUsedAttribute( 'opacity' );
    this.aLeavingSlide.insertBefore( this.aColorPlaneElement );
    this.aEnteringSlide.setOpacity( 0.0 );
    this.aEnteringSlide.show();
};

/** end
 *  This method removes the color plane element.
 */
FadingOverColorSlideChange.prototype.end = function()
{
    FadingOverColorSlideChange.superclass.end.call( this );
    this.aLeavingSlide.removeElement( this.aColorPlaneElement );
};

/** performIn
 *  This method set the opacity of the entering slide according to the passed
 *  time value.
 *
 *  @param nT
 *      The time parameter.
 */
FadingOverColorSlideChange.prototype.performIn = function( nT )
{
    this.aEnteringSlide.setOpacity( (nT > 0.55) ? 2.0*(nT-0.55) : 0.0 );
};

/** performOut
 *  This method set the opacity of the leaving slide according to the passed
 *  time value.
 *
 *  @param nT
 *      The time parameter.
 */
FadingOverColorSlideChange.prototype.performOut = function( nT )
{
    this.aLeavingSlide.setOpacity( (nT > 0.45) ? 0.0 : 2.0*(0.45-nT) );
};

FadingOverColorSlideChange.prototype.createColorPlaneElement = function()
{
    var aColorPlaneElement = document.createElementNS( NSS['svg'], 'rect' );
    aColorPlaneElement.setAttribute( 'width', String( this.aLeavingSlide.getWidth() ) );
    aColorPlaneElement.setAttribute( 'height', String( this.aLeavingSlide.getHeight() ) );
    aColorPlaneElement.setAttribute( 'fill', this.sFadeColor );
    return aColorPlaneElement;
};




/** Class MovingSlideChange
 *  This class performs a slide transition that involves translating the leaving
 *  slide and/or the entering one in a given direction.
 *
 *  @param aLeavingSlide
 *      An object of type AnimatedSlide handling the leaving slide.
 *  @param aEnteringSlide
 *      An object of type AnimatedSlide handling the entering slide.
 *  @param aLeavingDirection
 *      A 2D vector object {x, y}.
 *  @param aEnteringDirection
 *      A 2D vector object {x, y}.
 */
function MovingSlideChange( aLeavingSlide, aEnteringSlide,
                            aLeavingDirection, aEnteringDirection )
{
    MovingSlideChange.superclass.constructor.call( this, aLeavingSlide, aEnteringSlide );
    this.aLeavingDirection = aLeavingDirection;
    this.aEnteringDirection = aEnteringDirection;
}
extend( MovingSlideChange, SlideChangeBase );

/** start
 *  This method notifies to the slides involved in the transition the attributes
 *  appended to the slide elements for performing the animation.
 *  Moreover it sets the entering slide in the initial state and makes the slide
 *  visible.
 */
MovingSlideChange.prototype.start = function()
{
    MovingSlideChange.superclass.start.call( this );
    this.aEnteringSlide.notifyUsedAttribute( 'transform' );
    this.aLeavingSlide.notifyUsedAttribute( 'transform' );
    // Before setting the 'visibility' attribute of the entering slide to 'visible'
    // we translate it to the initial position so that it is not really visible
    // because it is clipped out.
    this.performIn( 0 );
    this.aEnteringSlide.show();
};

/** performIn
 *  This method set the position of the entering slide according to the passed
 *  time value.
 *
 *  @param nT
 *      The time parameter.
 */
MovingSlideChange.prototype.performIn = function( nT )
{
    var nS = nT - 1;
    var dx = nS * this.aEnteringDirection.x * this.aEnteringSlide.getWidth();
    var dy = nS * this.aEnteringDirection.y * this.aEnteringSlide.getHeight();
    this.aEnteringSlide.translate( dx, dy );
};

/** performOut
 *  This method set the position of the leaving slide according to the passed
 *  time value.
 *
 *  @param nT
 *      The time parameter.
 */
MovingSlideChange.prototype.performOut = function( nT )
{
    var dx = nT * this.aLeavingDirection.x * this.aLeavingSlide.getWidth();
    var dy = nT * this.aLeavingDirection.y * this.aLeavingSlide.getHeight();
    this.aLeavingSlide.translate( dx, dy );
};




/** Class ClippedSlideChange
 *  This class performs a slide transition where the entering slide wipes
 *  the leaving one out. The wipe effect is achieved by clipping the entering
 *  slide with a parametric path.
 *
 *  @param aLeavingSlide
 *      An object of type AnimatedSlide handling the leaving slide.
 *  @param aEnteringSlide
 *      An object of type AnimatedSlide handling the entering slide.
 *  @param aParametricPolyPolygon
 *      An object handling a <path> element that depends on a parameter.
 *  @param aTransitionInfo
 *      The set of parameters defining the slide transition to be performed.
 *  @param bIsDirectionForward
 *      The direction the slide transition has to be performed.
 */
function ClippedSlideChange( aLeavingSlide, aEnteringSlide, aParametricPolyPolygon,
                             aTransitionInfo, bIsDirectionForward )
{
    ClippedSlideChange.superclass.constructor.call( this, aLeavingSlide, aEnteringSlide );

    var bIsModeIn = true;
    this.aClippingFunctor= new ClippingFunctor( aParametricPolyPolygon, aTransitionInfo,
                                                bIsDirectionForward, bIsModeIn );
}
extend( ClippedSlideChange, SlideChangeBase );

/** start
 *  This method notifies to the slides involved in the transition the attributes
 *  appended to the slide elements for performing the animation.
 *  Moreover it sets the entering slide in the initial state and makes the slide
 *  visible.
 */
ClippedSlideChange.prototype.start = function()
{
    ClippedSlideChange.superclass.start.call( this );
    this.aEnteringSlide.notifyUsedAttribute( 'clip-path' );
    this.performIn( 0 );
    this.aEnteringSlide.show();
};

/** performIn
 *  This method set the position of the entering slide according to the passed
 *  time value.
 *
 *  @param nT
 *      The time parameter.
 */
ClippedSlideChange.prototype.performIn = function( nT )
{
    var nWidth = this.aEnteringSlide.getWidth();
    var nHeight = this.aEnteringSlide.getHeight();
    var aPolyPolygonElement = this.aClippingFunctor.perform( nT, nWidth, nHeight );
    this.aEnteringSlide.setClipPath( aPolyPolygonElement );
};

ClippedSlideChange.prototype.performOut = function( )
{
    // empty body
};




/** Class ClippingFunctor
 *  This class is responsible for computing the <path> used for clipping
 *  the entering slide in a polypolygon clipping slide transition or the
 *  animated shape in a transition filter effect.
 *
 *  @param aParametricPolyPolygon
 *      An object that handle a <path> element defined in the [0,1]x[0,1]
 *      unit square and that depends on a parameter.
 *  @param aTransitionInfo
 *      The set of parameters defining the slide transition to be performed.
 *  @param bIsDirectionForward
 *      The direction the slide transition has to be performed.
 *  @param bIsModeIn
 *      The direction the filter effect has to be performed
 */
function ClippingFunctor( aParametricPolyPolygon, aTransitionInfo,
                          bIsDirectionForward, bIsModeIn)
{
    this.aParametricPolyPolygon = aParametricPolyPolygon;
    this.aStaticTransformation = null;
    this.bForwardParameterSweep = true;
    this.bSubtractPolygon = false;
    this.bScaleIsotropically = aTransitionInfo.scaleIsotropically;
    this.bFlip = false;

    assert( this.aParametricPolyPolygon,
            'ClippingFunctor: parametric polygon is not valid' );

    if( aTransitionInfo.rotationAngle != 0.0 ||
        aTransitionInfo.scaleX != 1.0 ||  aTransitionInfo.scaleY != 1.0 )
    {
        // note: operations must be defined in reverse order.
        this.aStaticTransformation = SVGIdentityMatrix.translate( 0.5, 0.5 );
        if( aTransitionInfo.scaleX != 1.0 ||  aTransitionInfo.scaleY != 1.0 )
            this.aStaticTransformation
                = this.aStaticTransformation.scaleNonUniform( aTransitionInfo.scaleX,
                                                              aTransitionInfo.scaleY );
        if( aTransitionInfo.rotationAngle != 0.0 )
            this.aStaticTransformation
                = this.aStaticTransformation.rotate( aTransitionInfo.rotationAngle );
        this.aStaticTransformation = this.aStaticTransformation.translate( -0.5, -0.5 );
    }
    else
    {
        this.aStaticTransformation = document.documentElement.createSVGMatrix();
    }

    if( !bIsDirectionForward )
    {
        var aMatrix = null;
        switch( aTransitionInfo.reverseMethod )
        {
            default:
                log( 'ClippingFunctor: unexpected reverse method.' );
                break;
            case REVERSEMETHOD_IGNORE:
                break;
            case REVERSEMETHOD_INVERT_SWEEP:
                this.bForwardParameterSweep = !this.bForwardParameterSweep;
                break;
            case REVERSEMETHOD_SUBTRACT_POLYGON:
                this.bSubtractPolygon = !this.bSubtractPolygon;
                break;
            case REVERSEMETHOD_SUBTRACT_AND_INVERT:
                this.bForwardParameterSweep = !this.bForwardParameterSweep;
                this.bSubtractPolygon = !this.bSubtractPolygon;
                break;
            case REVERSEMETHOD_ROTATE_180:
                aMatrix = document.documentElement.createSVGMatrix();
                aMatrix.setToRotationAroundPoint( 0.5, 0.5, 180 );
                this.aStaticTransformation = aMatrix.multiply( this.aStaticTransformation );
                break;
            case REVERSEMETHOD_FLIP_X:
                aMatrix = document.documentElement.createSVGMatrix();
                // |-1  0  1 |
                // | 0  1  0 |
                aMatrix.a = -1; aMatrix.e = 1.0;
                this.aStaticTransformation = aMatrix.multiply( this.aStaticTransformation );
                this.bFlip = true;
                break;
            case REVERSEMETHOD_FLIP_Y:
                aMatrix = document.documentElement.createSVGMatrix();
                // | 1  0  0 |
                // | 0 -1  1 |
                aMatrix.d = -1; aMatrix.f = 1.0;
                this.aStaticTransformation = aMatrix.multiply( this.aStaticTransformation );
                this.bFlip = true;
                break;
        }
    }

    if( !bIsModeIn )
    {
        if( aTransitionInfo.outInvertsSweep )
        {
            this.bForwardParameterSweep = !this.bForwardParameterSweep;
        }
        else
        {
            this.bSubtractPolygon = !this.bSubtractPolygon;
        }
    }
}

// This path is used when the direction is the reverse one and
// the reverse method type is the subtraction type.
ClippingFunctor.aBoundingPath = document.createElementNS( NSS['svg'], 'path' );
ClippingFunctor.aBoundingPath.setAttribute( 'd', 'M -1 -1 L 2 -1 L 2 2 L -1 2 L -1 -1' );

/** perform
 *
 *  @param nT
 *      A parameter in [0,1] representing normalized time.
 *  @param nWidth
 *      The width of the bounding box of the slide/shape to be clipped.
 *  @param nHeight
 *      The height of the bounding box of the slide/shape to be clipped.
 *  @return SVGPathElement
 *      A svg <path> element representing the path to be used for the clipping
 *      operation.
 */
ClippingFunctor.prototype.perform = function( nT, nWidth, nHeight )
{
    var aClipPoly = this.aParametricPolyPolygon.perform( this.bForwardParameterSweep ? nT : (1 - nT) );

    // Note: even if the reverse method involves flipping we don't need to
    // change the clip-poly orientation because we utilize the 'nonzero'
    // clip-rule.
    // See: http://www.w3.org/TR/SVG11/painting.html#FillRuleProperty

    if( this.bSubtractPolygon )
    {
        aClipPoly.changeOrientation();
        aClipPoly.prependPath( ClippingFunctor.aBoundingPath );
    }

    var aMatrix;
    if( this.bScaleIsotropically )
    {
        var nScaleFactor = Math.max( nWidth, nHeight );
        // note: operations must be defined in reverse order.
        aMatrix = SVGIdentityMatrix.translate( -( nScaleFactor - nWidth ) / 2.0,
                                                  -( nScaleFactor - nHeight ) / 2.0 );
        aMatrix = aMatrix.scale( nScaleFactor );
        aMatrix = aMatrix.multiply( this.aStaticTransformation );
    }
    else
    {
        aMatrix = SVGIdentityMatrix.scaleNonUniform( nWidth, nHeight );
        aMatrix = aMatrix.multiply( this.aStaticTransformation );
    }

    aClipPoly.matrixTransform( aMatrix );

    return aClipPoly;
};




/** createClipPolyPolygon
 *
 *  @param nType
 *      An enumerator representing the transition type.
 *  @param nSubtype
 *      An enumerator representing the transition subtype.
 *  @return
 *      An object that handles a parametric <path> element.
 */
function createClipPolyPolygon( nType, nSubtype )
{
    switch( nType )
    {
        default:
            log( 'createClipPolyPolygon: unknown transition type: ' + nType );
            return null;
        case BARWIPE_TRANSITION:
            return new BarWipePath( 1 );
        case FOURBOXWIPE_TRANSITION:
            return new FourBoxWipePath( nSubtype === CORNERSOUT_TRANS_SUBTYPE );
        case BOXWIPE_TRANSITION:
            return new BoxWipePath( nSubtype == LEFTCENTER_TRANS_SUBTYPE ||
                                    nSubtype == TOPCENTER_TRANS_SUBTYPE ||
                                    nSubtype == RIGHTCENTER_TRANS_SUBTYPE ||
                                    nSubtype == BOTTOMCENTER_TRANS_SUBTYPE );
        case ELLIPSEWIPE_TRANSITION:
            return new EllipseWipePath( nSubtype );
        case FANWIPE_TRANSITION:
            return new FanWipePath(nSubtype == CENTERTOP_TRANS_SUBTYPE ||
                                   nSubtype == CENTERRIGHT_TRANS_SUBTYPE, true, false);
        case PINWHEELWIPE_TRANSITION:
            var nBlades;
            switch( nSubtype )
            {
                case ONEBLADE_TRANS_SUBTYPE:
                    nBlades = 1;
                    break;
                case DEFAULT_TRANS_SUBTYPE:
                case TWOBLADEVERTICAL_TRANS_SUBTYPE:
                    nBlades = 2;
                    break;
                case TWOBLADEHORIZONTAL_TRANS_SUBTYPE:
                    nBlades = 2;
                    break;
                case THREEBLADE_TRANS_SUBTYPE:
                    nBlades = 3;
                    break;
                case FOURBLADE_TRANS_SUBTYPE:
                    nBlades = 4;
                    break;
                case EIGHTBLADE_TRANS_SUBTYPE:
                    nBlades = 8;
                    break;
                default:
                    log( 'createClipPolyPolygon: unknown subtype: ' + nSubtype );
                    return null;
            }
            return new PinWheelWipePath( nBlades );
        case CLOCKWIPE_TRANSITION:
            return new ClockWipePath();
        case RANDOMBARWIPE_TRANSITION:
            return new RandomWipePath( 128, true /* bars */ );
        case CHECKERBOARDWIPE_TRANSITION:
            return new CheckerBoardWipePath( 10 );
        case ZIGZAGWIPE_TRANSITION:
            return new ZigZagWipePath( 5 );
        case BARNZIGZAGWIPE_TRANSITION:
            return new BarnZigZagWipePath( 5 );
        case IRISWIPE_TRANSITION:
            switch(nSubtype)
            {
                case RECTANGLE_TRANS_SUBTYPE:
                    return new IrisWipePath(0);
                case DIAMOND_TRANS_SUBTYPE:
                    return new IrisWipePath(1);
                default:
                    log( 'createClipPolyPolygon: unknown subtype: ' + nSubtype );
                    return null;
            }
        case BARNDOORWIPE_TRANSITION:
            return new BarnDoorWipePath(false);
        case SINGLESWEEPWIPE_TRANSITION:
            return new SweepWipePath(
                // center
                nSubtype == CLOCKWISETOP_TRANS_SUBTYPE ||
                nSubtype == CLOCKWISERIGHT_TRANS_SUBTYPE ||
                nSubtype == CLOCKWISEBOTTOM_TRANS_SUBTYPE ||
                nSubtype == CLOCKWISELEFT_TRANS_SUBTYPE,
                // single
                true,
                // oppositeVertical
                false,
                // flipOnYAxis
                nSubtype == COUNTERCLOCKWISEBOTTOMLEFT_TRANS_SUBTYPE ||
                nSubtype == COUNTERCLOCKWISETOPRIGHT_TRANS_SUBTYPE );
        case WATERFALLWIPE_TRANSITION:
            return new WaterfallWipePath(128, // flipOnYAxis
                                              nSubtype == VERTICALRIGHT_TRANS_SUBTYPE ||
                                              nSubtype == HORIZONTALLEFT_TRANS_SUBTYPE);
        case MISCDIAGONALWIPE_TRANSITION:
            switch(nSubtype) {
                case DOUBLEBARNDOOR_TRANS_SUBTYPE:
                    return new BarnDoorWipePath(true /* Doubled */);
                case DOUBLEDIAMOND_TRANS_SUBTYPE:
                    return new DoubleDiamondWipePath();
                default:
                    log( 'createClipPolyPolygon: unhandled subtype: ' + nSubtype );
                    return null;
            }
        case DISSOLVE_TRANSITION:
            return new RandomWipePath( 16 * 16, false /* dissolve */ );
        case VEEWIPE_TRANSITION:
            return new VeeWipePath();
        case SNAKEWIPE_TRANSITION:
            return new SnakeWipePath( 8 * 8, // diagonal
                                             nSubtype == TOPLEFTDIAGONAL_TRANS_SUBTYPE     ||
                                             nSubtype == TOPRIGHTDIAGONAL_TRANS_SUBTYPE    ||
                                             nSubtype == BOTTOMRIGHTDIAGONAL_TRANS_SUBTYPE ||
                                             nSubtype == BOTTOMLEFTDIAGONAL_TRANS_SUBTYPE   ,
                                             // flipOnYAxis
                                             nSubtype == TOPLEFTVERTICAL_TRANS_SUBTYPE     ||
                                             nSubtype == TOPRIGHTDIAGONAL_TRANS_SUBTYPE    ||
                                             nSubtype == BOTTOMLEFTDIAGONAL_TRANS_SUBTYPE
                                             );
        case PARALLELSNAKESWIPE_TRANSITION:
            return new ParallelSnakesWipePath(
                8 * 8, // elements
                // diagonal
                nSubtype == DIAGONALBOTTOMLEFTOPPOSITE_TRANS_SUBTYPE ||
                nSubtype == DIAGONALTOPLEFTOPPOSITE_TRANS_SUBTYPE,
                // flipOnYAxis
                nSubtype == VERTICALBOTTOMLEFTOPPOSITE_TRANS_SUBTYPE ||
                nSubtype == HORIZONTALTOPLEFTOPPOSITE_TRANS_SUBTYPE  ||
                nSubtype == DIAGONALTOPLEFTOPPOSITE_TRANS_SUBTYPE,
                // opposite
                nSubtype == VERTICALTOPLEFTOPPOSITE_TRANS_SUBTYPE    ||
                nSubtype == VERTICALBOTTOMLEFTOPPOSITE_TRANS_SUBTYPE ||
                nSubtype == HORIZONTALTOPLEFTOPPOSITE_TRANS_SUBTYPE  ||
                nSubtype == HORIZONTALTOPRIGHTOPPOSITE_TRANS_SUBTYPE ||
                nSubtype == DIAGONALBOTTOMLEFTOPPOSITE_TRANS_SUBTYPE ||
                nSubtype == DIAGONALTOPLEFTOPPOSITE_TRANS_SUBTYPE
                );

        case SPIRALWIPE_TRANSITION:
            return new SpiralWipePath(
                8 * 8, // elements
                nSubtype == TOPLEFTCOUNTERCLOCKWISE_TRANS_SUBTYPE     ||
                nSubtype == TOPRIGHTCOUNTERCLOCKWISE_TRANS_SUBTYPE    ||
                nSubtype == BOTTOMRIGHTCOUNTERCLOCKWISE_TRANS_SUBTYPE ||
                nSubtype == BOTTOMLEFTCOUNTERCLOCKWISE_TRANS_SUBTYPE );

        case BOXSNAKESWIPE_TRANSITION:
            return new BoxSnakesWipePath(
                // elements
                8 * 8,
                // fourBox
                nSubtype == FOURBOXVERTICAL_TRANS_SUBTYPE ||
                nSubtype == FOURBOXHORIZONTAL_TRANS_SUBTYPE );
    }
}




function createUnitSquarePath()
{
    var aPath = document.createElementNS( NSS['svg'], 'path' );
    var sD = 'M 0 0 L 1 0 L 1 1 L 0 1 L 0 0';
    aPath.setAttribute( 'd', sD );
    return aPath;
}

function createEmptyPath()
{
    var aPath = document.createElementNS( NSS['svg'], 'path' );
    var sD = 'M 0 0 L 0 0';
    aPath.setAttribute( 'd', sD );
    return aPath;
}

function pruneScaleValue( nVal )
{
    if( nVal < 0.0 )
        return (nVal < -0.00001 ? nVal : -0.00001);
    else
        return (nVal > 0.00001 ? nVal : 0.00001);
}


/** Class BarWipePath
 *  This class handles a <path> element that defines a unit square and
 *  transforms it accordingly to a parameter in the [0,1] range for performing
 *  a left to right barWipe transition.
 *
 *  @param nBars
 *     The number of bars to be generated.
 */
function BarWipePath( nBars /* nBars > 1: blinds effect */ )
{
    this.nBars = nBars;
    if( this.nBars === undefined || this.nBars < 1 )
        this.nBars = 1;
    this.aBasePath = createUnitSquarePath();
}

/** perform
 *
 *  @param nT
 *      A parameter in [0,1] representing the width of the generated bars.
 *  @return SVGPathElement
 *      A svg <path> element representing a multi-bars.
 */
BarWipePath.prototype.perform = function( nT )
{

    var aMatrix = SVGIdentityMatrix.scaleNonUniform( pruneScaleValue( nT / this.nBars ), 1.0 );

    var aPolyPath = this.aBasePath.cloneNode( true );
    aPolyPath.matrixTransform( aMatrix );

    if( this.nBars > 1 )
    {
        var i;
        var aTransform;
        var aPath;
        for( i = this.nBars - 1; i > 0; --i )
        {
            aTransform = SVGIdentityMatrix.translate( i / this.nBars, 0.0 );
            aTransform = aTransform.multiply( aMatrix );
            aPath = this.aBasePath.cloneNode( true );
            aPath.matrixTransform( aTransform );
            aPolyPath.appendPath( aPath );
        }
    }
    return aPolyPath;
};


/** Class BoxWipePath
 *  This class handles a path made up by one square and is utilized for
 *  performing BoxWipe transitions.
 *
 *  @param bIsTopCentered
 *      if true the transition subtype is top centered else not.
 */
function BoxWipePath(bIsTopCentered) {
    this.bIsTopCentered = bIsTopCentered;
    this.aBasePath = createUnitSquarePath();
}

BoxWipePath.prototype.perform = function( nT ) {
    var d = pruneScaleValue(nT);
    var aTransform = SVGIdentityMatrix;
    if(this.bIsTopCentered) {
        aTransform = aTransform.translate(-0.5, 0.0).scale(d, d).translate(0.5, 0.0);
    }
    else {
        aTransform = aTransform.scale(d, d);
    }
    var aPath = this.aBasePath.cloneNode(true);
    aPath.matrixTransform(aTransform);
    return aPath;
}

/* Class SweepWipePath
 *
 *
 */
function SweepWipePath(bCenter, bSingle, bOppositeVertical, bFlipOnYAxis) {
  this.bCenter = bCenter;
  this.bSingle = bSingle;
  this.bOppositeVertical = bOppositeVertical;
  this.bFlipOnYAxis = bFlipOnYAxis;
  this.aBasePath = createUnitSquarePath();
}

SweepWipePath.prototype.perform = function( nT ) {
    nT /= 2.0;
    if(!this.bCenter)
        nT /= 2.0;
    if(!this.bSingle && !this.bOppositeVertical)
        nT /= 2.0;

    var poly = PinWheelWipePath.calcCenteredClock( nT + 0.25, 1.0 );
    var aTransform;

    if(this.bCenter) {
        aTransform = SVGIdentityMatrix.translate(0.5, 0.0);
        poly.matrixTransform(aTransform);
    }
    var res = poly;

    if(!this.bSingle) {
        if(this.bOppositeVertical) {
            aTransform = SVGIdentityMatrix.scale(1.0, -1.0);
            aTransform.translate(0.0, 1.0);
            poly.matrixTransform(aTransform);
            poly.changeOrientation();
        }
        else {
            aTransform = SVGIdentityMatrix.translate(-0.5, -0.5);
            aTransform.rotate(Math.PI);
            aTransform.translate(0.5, 0.5);
            poly.matrixTransform(aTransform);
        }
        res.appendPath(poly);
    }
    return this.bFlipOnYAxis ? flipOnYAxis(res) : res;
}

/** Class FourBoxWipePath
 *  This class handles a path made up by four squares and is utilized for
 *  performing fourBoxWipe transitions.
 *
 *  @param bCornersOut
 *      If true the transition subtype is cornersOut else is cornersIn.
 */
function FourBoxWipePath( bCornersOut )
{
    this.bCornersOut = bCornersOut;
    this.aBasePath = createUnitSquarePath();
}

FourBoxWipePath.prototype.perform = function( nT )
{
    var aMatrix;
    var d = pruneScaleValue( nT / 2.0 );

    if( this.bCornersOut )
    {
        aMatrix = SVGIdentityMatrix.translate( -0.25, -0.25 ).scale( d ).translate( -0.5, -0.5 );
    }
    else
    {
        aMatrix = SVGIdentityMatrix.translate( -0.5, -0.5 ).scale( d );
    }


    var aTransform = aMatrix;
    // top left
    var aSquare = this.aBasePath.cloneNode( true );
    aSquare.matrixTransform( aTransform );
    var aPolyPath = aSquare;
    // bottom left, flip on x-axis:
    aMatrix = SVGIdentityMatrix.flipY();
    aTransform = aMatrix.multiply( aTransform );
    aSquare = this.aBasePath.cloneNode( true );
    aSquare.matrixTransform( aTransform );
    aSquare.changeOrientation();
    aPolyPath.appendPath( aSquare );
    // bottom right, flip on y-axis:
    aMatrix = SVGIdentityMatrix.flipX();
    aTransform = aMatrix.multiply( aTransform );
    aSquare = this.aBasePath.cloneNode( true );
    aSquare.matrixTransform( aTransform );
    aPolyPath.appendPath( aSquare );
    // top right, flip on x-axis:
    aMatrix = SVGIdentityMatrix.flipY();
    aTransform = aMatrix.multiply( aTransform );
    aSquare = this.aBasePath.cloneNode( true );
    aSquare.matrixTransform( aTransform );
    aSquare.changeOrientation();
    aPolyPath.appendPath( aSquare );

    // Remind: operations are applied in inverse order
    aMatrix = SVGIdentityMatrix.translate( 0.5, 0.5 );
    // We enlarge a bit the clip path so we avoid that in reverse direction
    // some thin line of the border stroke is visible.
    aMatrix = aMatrix.scale( 1.1 );
    aPolyPath.matrixTransform( aMatrix );

    return aPolyPath;
};




/** Class EllipseWipePath
 *  This class handles a parametric ellipse represented by a path made up of
 *  cubic Bezier curve segments that helps in performing the ellipseWipe
 *  transition.
 *
 *  @param eSubtype
 *      The transition subtype.
 */
function EllipseWipePath( eSubtype )
{
    this.eSubtype = eSubtype;

    // precomputed circle( 0.5, 0.5, SQRT2 / 2 )
    var sPathData = 'M 0.5 -0.207107 ' +
                    'C 0.687536 -0.207107 0.867392 -0.132608 1 0 ' +
                    'C 1.13261 0.132608 1.20711 0.312464 1.20711 0.5 ' +
                    'C 1.20711 0.687536 1.13261 0.867392 1 1 ' +
                    'C 0.867392 1.13261 0.687536 1.20711 0.5 1.20711 ' +
                    'C 0.312464 1.20711 0.132608 1.13261 0 1 ' +
                    'C -0.132608 0.867392 -0.207107 0.687536 -0.207107 0.5 ' +
                    'C -0.207107 0.312464 -0.132608 0.132608 0 0 ' +
                    'C 0.132608 -0.132608 0.312464 -0.207107 0.5 -0.207107';

    this.aBasePath = document.createElementNS( NSS['svg'], 'path' );
    this.aBasePath.setAttribute( 'd', sPathData );
}

EllipseWipePath.prototype.perform = function( nT )
{

    var aTransform = SVGIdentityMatrix.translate( 0.5, 0.5 ).scale( nT ).translate( -0.5, -0.5 );
    var aEllipse = this.aBasePath.cloneNode( true );
    aEllipse.matrixTransform( aTransform );

    return aEllipse;
};

/*
 * Class FanWipePath
 *
 */
function FanWipePath(bIsCenter, bIsSingle, bIsFanIn) {
    this.bCenter = bIsCenter;
    this.bSingle = bIsSingle;
    this.bFanIn  = bIsFanIn;
    this.aBasePath = createUnitSquarePath();
}

FanWipePath.prototype.perform = function( nT ) {
  var res = this.aBasePath.cloneNode(true);
  var poly = PinWheelWipePath.calcCenteredClock(
          nT / ((this.bCenter && this.bSingle) ? 2.0 : 4.0), 1.0);
  res.appendPath(poly);
  // flip on y-axis
  var aTransform = SVGIdentityMatrix.flipY();
  aTransform = aTransform.scaleNonUniform(-1.0, 1.0);
  poly.matrixTransform(aTransform);
  res.appendPath(poly);

  if(this.bCenter) {
      aTransform = SVGIdentityMatrix.scaleNonUniform(0.5, 0.5).translate(0.5, 0.5);
      res.matrixTransform(aTransform);

      if(!this.bSingle)
          res.appendPath(flipOnXAxis(res));
  }
  else {
      aTransform = SVGIdentityMatrix.scaleNonUniform(0.5, 1.0).translate(0.5, 1.0);
      res.matrixTransform(aTransform);
  }
  return res;
}

/**
 * Class ClockWipePath
 *
 */
function ClockWipePath() { }

ClockWipePath.prototype.perform = function( nT ) {
    const aTransform = SVGIdentityMatrix.scaleNonUniform(0.5, 0.5).translate(0.5, 0.5);
    var aPolyPath = PinWheelWipePath.calcCenteredClock(nT, 1.0);
    aPolyPath.matrixTransform( aTransform );

    return aPolyPath;
}

/** Class PinWheelWipePath
 *  This class handles a parametric poly-path that is used for performing
 *  a spinWheelWipe transition.
 *
 *  @param nBlades
 *      Number of blades generated by the transition.
 */
function PinWheelWipePath( nBlades )
{
    this.nBlades = nBlades;
    if( !this.nBlades || this.nBlades < 1 )
        this.nBlades = 1;
}

PinWheelWipePath.calcCenteredClock = function( nT, nE )
{
    var nMAX_EDGE = 2;

    var aTransform = SVGIdentityMatrix.rotate( nT * 360 );

    var aPoint = document.documentElement.createSVGPoint();
    aPoint.y = -nMAX_EDGE;
    aPoint = aPoint.matrixTransform( aTransform );

    var sPathData = 'M ' + aPoint.x + ' ' + aPoint.y + ' ';
    if( nT >= 0.875 )
        // L -e -e
        sPathData += 'L ' + '-' + nE + ' -' + nE + ' ';
    if( nT >= 0.625 )
        // L -e e
        sPathData += 'L ' + '-' + nE + ' ' + nE + ' ';
    if( nT >= 0.375 )
        // L e e
        sPathData += 'L ' + nE + ' ' + nE + ' ';
     if( nT >= 0.125 )
        // L e -e
        sPathData += 'L ' + nE + ' -' + nE + ' ';

    // L 0 -e
    sPathData += 'L 0 -' + nE + ' ';
    sPathData += 'L 0 0 ';
    // Z
    sPathData += 'L '  + aPoint.x + ' ' + aPoint.y;

    var aPath = document.createElementNS( NSS['svg'], 'path' );
    aPath.setAttribute( 'd', sPathData );
    return aPath;
};

PinWheelWipePath.prototype.perform = function( nT )
{
    var aBasePath = PinWheelWipePath.calcCenteredClock( nT / this.nBlades,
                                                        2.0 /* max edge when rotating */  );

    var aPolyPath = aBasePath.cloneNode( true );
    var aPath;
    var aRotation;
    var i;
    for( i = this.nBlades - 1; i > 0; --i )
    {
        aRotation = SVGIdentityMatrix.rotate( (i * 360) / this.nBlades );
        aPath = aBasePath.cloneNode( true );
        aPath.matrixTransform( aRotation );
        aPolyPath.appendPath( aPath );
    }

    var aTransform = SVGIdentityMatrix.translate( 0.5, 0.5 ).scale( 0.5 );
    aPolyPath.matrixTransform( aTransform );

    return aPolyPath;
};

/** Class BarnDoorWipe
  *
  * @param doubled
  */
function BarnDoorWipePath(doubled) {
    this.aBasePath = createUnitSquarePath();
    this.doubled   = doubled;
}

BarnDoorWipePath.prototype.perform = function( nT ) {
    if(this.doubled)
        nT /= 2.0;
    var aTransform = SVGIdentityMatrix.translate(-0.5, -0.5);
    aTransform = aTransform.scaleNonUniform(pruneScaleValue(nT), 1.0).translate(0.5, 0.5);
    var aPath = this.aBasePath.cloneNode(true);
    aPath.matrixTransform(aTransform);
    var res = aPath;

    if(this.doubled) {
        aTransform = SVGIdentityMatrix.translate(-0.5, -0.5);
        aTransform = aTransform.rotate(Math.PI / 2).translate(0.5, 0.5);
        aPath.matrixTransform(aTransform);
        res.appendPath(aPath);
    }
    return res;
}

/** Class WaterfallWipe
  *
  * @param nElements
  *     Number of cells to be used
  * @param bFlipOnYAxis
  *     Whether to flip on y-axis or not.
  */
function WaterfallWipePath(nElements, bFlipOnYAxis) {
    this.bFlipOnYAxis = bFlipOnYAxis;

    var sqrtElements = Math.floor(Math.sqrt(nElements));
    var elementEdge = 1.0/sqrtElements;

    var aPath = 'M '+ 0.0 + ' ' + -1.0 + ' ';
    for(var pos = sqrtElements; pos--; ) {
        var xPos = sqrtElements - pos - 1;
        var yPos = pruneScaleValue( ((pos+1) * elementEdge) - 1.0);

        aPath += 'L ' + pruneScaleValue(xPos * elementEdge) + ' ' + yPos + ' ';
        aPath += 'L ' + pruneScaleValue((xPos+1)*elementEdge) + ' ' + yPos + ' ';
    }
    aPath += 'L ' + 1.0 + ' ' + -1.0 + ' ';
    aPath += 'L ' + 0.0 + ' ' + -1.0 + ' ';
    this.aBasePath = document.createElementNS( NSS['svg'], 'path');
    this.aBasePath.setAttribute('d', aPath);
}

WaterfallWipePath.prototype.perform = function( nT ) {
    var poly = this.aBasePath.cloneNode(true);
    var aTransform = SVGIdentityMatrix.translate(0.0, pruneScaleValue(2.0 * nT));
    poly.matrixTransform(aTransform);
    var aHead = 'M ' + 0.0 + ' ' + -1.0 + ' ';
    var aHeadPath= document.createElementNS( NSS['svg'], 'path');
    aHeadPath.setAttribute('d', aHead);

    var aTail = 'M ' + 1.0 + ' ' + -1.0 + ' ';
    var aTailPath = document.createElementNS( NSS['svg'], 'path');
    aTailPath.setAttribute('d', aTail);

    poly.prependPath(aHeadPath);
    poly.appendPath(aTailPath);

    return this.bFlipOnYAxis ? flipOnYAxis(poly) : poly;
}

/** Class DoubleDiamondWipePath
 *
 */
function DoubleDiamondWipePath() { }

DoubleDiamondWipePath.prototype.perform = function( nT ) {
    var a = pruneScaleValue(0.25 + (nT * 0.75));
    var aPath = 'M ' + (0.5 + a) + ' ' + 0.5 + ' ';
    aPath += 'L ' + 0.5 + ' ' + (0.5 - a) + ' ';
    aPath += 'L ' + (0.5 - a) + ' ' + 0.5 + ' ';
    aPath += 'L ' + 0.5 + ' ' + (0.5 + a) + ' ';
    aPath += 'L ' + (0.5 + a) + ' ' + 0.5 + ' ';
    var poly = document.createElementNS( NSS['svg'], 'path');
    poly.setAttribute('d', aPath);
    var res = poly.cloneNode(true);

    var b = pruneScaleValue( (1.0 - nT) * 0.25);
    aPath = 'M ' + (0.5 + b) + ' ' + 0.5 + ' ';
    aPath += 'L ' + 0.5 + ' ' + (0.5 + b) + ' ';
    aPath += 'L ' + (0.5 - b) + ' ' + 0.5 + ' ';
    aPath += 'L ' + 0.5 + ' ' + (0.5 - b) + ' ';
    aPath += 'L ' + (0.5 + b) + ' ' + 0.5 + ' ';
    poly = document.createElementNS( NSS['svg'], 'path');
    poly.setAttribute('d', aPath);
    res.appendPath(poly);

    return res;
}

/** Class Iriswipe
  *
  * @param unitRect
  *
  */
function IrisWipePath(unitRect) {
    this.unitRect = unitRect;
    this.aBasePath = createUnitSquarePath();
}


/** perform
  *
  *  @param nT
  *      A parameter in [0,1] representing the diamond or rectangle.
  *  @return SVGPathElement
  *      A svg <path> element representing a transition.
  */
IrisWipePath.prototype.perform = function( nT ) {
    var d = pruneScaleValue(nT);
    var aTransform = SVGIdentityMatrix.translate(-0.5, -0.5);
    aTransform = aTransform.multiply(SVGIdentityMatrix.scaleNonUniform(d, d).translate(0.5, 0.5));
    var aPath = this.aBasePath.cloneNode(true);
    aPath.matrixTransform(aTransform);
    return aPath;
}

/**
 * Class ZigZagWipePath
 *
 * @param nZigs
 *
 */
function ZigZagWipePath(nZigs) {
    this.zigEdge = 1.0/nZigs;
    const d = this.zigEdge;
    const d2 = (d / 2.0);
    this.aBasePath = 'M ' + (-1.0 - d) + ' ' + -d + ' ';
    this.aBasePath += 'L ' + (-1.0 - d) + ' ' + (1.0 + d) + ' ';
    this.aBasePath += 'L ' + -d + ' ' + (1.0 + d) + ' ';

    for(var pos = (nZigs + 2); pos--; ) {
        this.aBasePath += 'L ' + 0.0 + ' ' + ((pos - 1) * d + d2) + ' ';
        this.aBasePath += 'L ' + -d + ' ' + (pos - 1) * d + ' ';
    }
    this.aBasePath += 'L ' + (-1.0 - d) + ' ' + -d + ' ';
}

ZigZagWipePath.prototype.perform = function( nT ) {
    var res = document.createElementNS( NSS['svg'], 'path');
    res.setAttribute('d', this.aBasePath);
    res.matrixTransform(SVGIdentityMatrix.translate((1.0 + this.zigEdge) * nT, 0.0));
    return res;
}

/*
 * Class BarnZigZagWipePath
 *
 * @param nZigs
 *
 */
function BarnZigZagWipePath( nZigs ) { ZigZagWipePath.call(this, nZigs); }

BarnZigZagWipePath.prototype = Object.create(ZigZagWipePath);

BarnZigZagWipePath.prototype.perform = function( nT ) {
    var res = createEmptyPath();
    var poly = document.createElementNS( NSS['svg'], 'path');
    var aTransform = SVGIdentityMatrix.translate(
        ((1.0 + this.zigEdge) * (1.0 - nT)) / 2.0, 0.0);
    poly.setAttribute('d', this.aBasePath);
    poly.changeOrientation();
    poly.matrixTransform(aTransform);
    res.appendPath(poly);

    aTransform = SVGIdentityMatrix.scale(-1.0, 1.0);
    aTransform.translate(1.0, this.zigEdge / 2.0);
    poly = document.createElementNS( NSS['svg'], 'path');
    poly.setAttribute('d', this.aBasePath);
    poly.matrixTransform(aTransform);
    res.appendPath(poly);

    return res;
}

/** Class CheckerBoardWipePath
 *
 *  @param unitsPerEdge
 *     The number of cells (per line and column) in the checker board.
 */
function CheckerBoardWipePath( unitsPerEdge )
{
    this.unitsPerEdge = unitsPerEdge;
    if( this.unitsPerEdge === undefined || this.unitsPerEdge < 1 )
        this.unitsPerEdge = 10;
    this.aBasePath = createUnitSquarePath();
}

/** perform
 *
 *  @param nT
 *      A parameter in [0,1] representing the width of the generated bars.
 *  @return SVGPathElement
 *      A svg <path> element representing a multi-bars.
 */
CheckerBoardWipePath.prototype.perform = function( nT )
{
    var d = pruneScaleValue(1.0 / this.unitsPerEdge);
    var aMatrix = SVGIdentityMatrix.scaleNonUniform(pruneScaleValue( d*2.0*nT ),
                                                    pruneScaleValue( d ) );

    var aPolyPath = null;
    var i, j;
    var aTransform;
    var aPath;
    for ( i = this.unitsPerEdge; i--; )
    {
        aTransform = SVGIdentityMatrix;

        if ((i % 2) == 1) // odd line
            aTransform = aTransform.translate( -d, 0.0 );

        aTransform = aTransform.multiply( aMatrix );

        for ( j = (this.unitsPerEdge / 2) + 1; j--;)
        {
            aPath = this.aBasePath.cloneNode( true );
            aPath.matrixTransform( aTransform );
            if (aPolyPath == null) aPolyPath = aPath;
            else aPolyPath.appendPath( aPath );
            aTransform = SVGIdentityMatrix.translate( d*2.0, 0.0 ).multiply( aTransform );
        }

        aMatrix = SVGIdentityMatrix.translate( 0.0, d ).multiply( aMatrix ); // next line
    }

    return aPolyPath;
};



/** Class RandomWipePath
 *
 *  @param nElements
 *     The number of bars or cells to be used.
 *  @param bRandomBars
 *     true: generates a horizontal random bar wipe
 *     false: generates a dissolve wipe
 */
function RandomWipePath( nElements, bRandomBars )
{
    this.nElements = nElements;
    this.aBasePath = createUnitSquarePath();
    this.aPositionArray = new Array( nElements );
    this.aClipPath = createEmptyPath();
    this.nAlreadyAppendedElements = 0;

    var fEdgeLength, nPos, aTransform;

    if( bRandomBars ) // random bar wipe
    {
        fEdgeLength = 1.0 / nElements;
        for( nPos = 0; nPos < nElements; ++nPos )
        {
            this.aPositionArray[nPos] = { x: 0.0, y: pruneScaleValue( nPos * fEdgeLength ) }
        }
        aTransform = SVGIdentityMatrix.scaleNonUniform( 1.0, pruneScaleValue( fEdgeLength ) );
    }
    else // dissolve wipe
    {
        var nSqrtElements = Math.round( Math.sqrt( nElements ) );
        fEdgeLength = 1.0 / nSqrtElements;
        for( nPos = 0; nPos < nElements; ++nPos )
        {
            this.aPositionArray[nPos] = {
                x: pruneScaleValue( ( nPos % nSqrtElements ) * fEdgeLength ),
                y: pruneScaleValue( ( nPos / nSqrtElements ) * fEdgeLength ) }
        }
        aTransform = SVGIdentityMatrix.scale( pruneScaleValue( fEdgeLength ) );
    }
    this.aBasePath.matrixTransform( aTransform );

    var nPos1, nPos2;
    var tmp;
    for( nPos1 = nElements - 1; nPos1 > 0; --nPos1 )
    {
        nPos2 = getRandomInt( nPos1 + 1 );
        tmp = this.aPositionArray[nPos1];
        this.aPositionArray[nPos1] = this.aPositionArray[nPos2];
        this.aPositionArray[nPos2] = tmp;
    }
}

/** perform
 *
 *  @param nT
 *      A parameter in [0,1] representing the width of the generated bars or squares.
 *  @return SVGPathElement
 *      A svg <path> element representing a multi bars or a multi squared cells.
 */
RandomWipePath.prototype.perform = function( nT )
{
    var aPolyPath = createEmptyPath();
    var aPoint;
    var aPath;
    var aTransform;
    var nElements = Math.round( nT * this.nElements );
    if( nElements === 0 )
    {
        return aPolyPath;
    }
    // check if we need to reset the clip path
    if( this.nAlreadyAppendedElements >= nElements )
    {
        this.nAlreadyAppendedElements = 0;
        this.aClipPath = createEmptyPath();
    }
    var nPos;
    for( nPos = this.nAlreadyAppendedElements; nPos < nElements; ++nPos )
    {
        aPoint = this.aPositionArray[nPos];
        aPath = this.aBasePath.cloneNode( true );
        aTransform = SVGIdentityMatrix.translate( aPoint.x, aPoint.y );
        aPath.matrixTransform( aTransform );
        aPolyPath.appendPath( aPath );
    }

    this.nAlreadyAppendedElements = nElements;
    this.aClipPath.appendPath( aPolyPath );

    return this.aClipPath.cloneNode( true );
};

/** Class SnakeWipeSlide
 *
 *  @param nElements
 *  @param bDiagonal
 *  @param bFlipOnYaxis
 */
function SnakeWipePath(nElements, bDiagonal, bflipOnYAxis)
{
    this.sqrtElements = Math.floor(Math.sqrt(nElements));
    this.elementEdge  = (1.0 / this.sqrtElements);
    this.diagonal     = bDiagonal;
    this.flipOnYAxis  = bflipOnYAxis;
    this.aBasePath    = createUnitSquarePath();
}

SnakeWipePath.prototype.calcSnake = function(t)
{
    var aPolyPath = createEmptyPath();
    const area   = (t * this.sqrtElements * this.sqrtElements);
    const line_  = Math.floor(area) / this.sqrtElements;
    const line   = pruneScaleValue(line_ / this.sqrtElements);
    const col    = pruneScaleValue((area - (line_ * this.sqrtElements)) / this.sqrtElements);

    if(line != 0) {
        let aPath = 'M '+ 0.0 + ' ' + 0.0 + ' ';
        aPath += 'L ' + 0.0 + ' ' + line + ' ';
        aPath += 'L ' + 1.0 + ' ' + line + ' ';
        aPath += 'L ' + 1.0 + ' ' + 0.0 + ' ';
        aPath += 'L 0 0 ';
        let poly = document.createElementNS( NSS['svg'], 'path');
        poly.setAttribute('d', aPath);
        aPolyPath.appendPath(poly);
    }
    if(col != 0) {
        var offset = 0.0;
        if((line_ & 1) == 1) {
            // odd line: => right to left
            offset = (1.0 - col);
        }
        let aPath = 'M ' + offset + ' ' + line + ' ';
        aPath += 'L '+ offset + ' ' + (line + this.elementEdge) + ' ';
        aPath += 'L ' + (offset+col) + ' ' + (line + this.elementEdge) + ' ';
        aPath += 'L ' + (offset+col) + ' ' + line + ' ';
        aPath += 'L ' + offset + ' ' + line + ' ';
        let poly = document.createElementNS( NSS['svg'], 'path');
        poly.setAttribute('d', aPath);
        aPolyPath.appendPath(poly);
    }

    return aPolyPath;
}

SnakeWipePath.prototype.calcHalfDiagonalSnake = function(nT, bIn) {
    var res = createEmptyPath();

    if(bIn) {
        const sqrtArea2 = Math.sqrt(nT * this.sqrtElements * this.sqrtElements);
        const edge = pruneScaleValue(sqrtArea2 / this.sqrtElements);

        var aPath, aPoint = document.documentElement.createSVGPoint();
        if(edge) {
            aPath = 'M ' + aPoint.x + ' ' + aPoint.y + ' ';
            aPoint.y = edge;
            aPath += 'L ' + aPoint.x + ' ' + aPoint.y + ' ';
            aPoint.x = edge;
            aPoint.y = 0.0;
            aPath += 'L ' + aPoint.x + ' ' + aPoint.y + ' ';
            aPoint.x = 0.0;
            aPath += 'L ' + aPoint.x + ' ' + aPoint.y + ' ';
            const poly = document.createElementNS( NSS['svg'], 'path');
            poly.setAttribute('d', aPath);
            res.appendPath(poly);
        }
        const a = (Math.SQRT1_2 / this.sqrtElements);
        const d = (sqrtArea2 - Math.floor(sqrtArea2));
        const len = (nT * Math.SQRT1_2 * d);
        const height = pruneScaleValue(Math.SQRT1_2 / this.sqrtElements);
        aPath = 'M ' + aPoint.x + ' ' + aPoint.y + ' ';
        aPoint.y = height;
        aPath += 'L ' + aPoint.x + ' ' + aPoint.y + ' ';
        aPoint.x = len + a;
        aPath += 'L ' + aPoint.x + ' ' + aPoint.y + ' ';
        aPoint.y = 0.0;
        aPath += 'L ' + aPoint.x + ' ' + aPoint.y + ' ';
        aPoint.x = 0.0;
        aPath += 'L ' + aPoint.x + ' ' + aPoint.y + ' ';
        const poly = document.createElementNS( NSS['svg'], 'path');
        poly.setAttribute('d', aPath);
        let aTransform;

        if((Math.floor(sqrtArea2) & 1) == 1) {
            // odd line
            aTransform = SVGIdentityMatrix.rotate((Math.PI)/2 + (Math.PI)/4);
            aTransform.translate(edge + this.elementEdge, 0.0);
        }
        else {
            aTransform = SVGIdentityMatrix.translate(-a, 0.0);
            aTransform.rotate(-(Math.PI/4));
            aTransform.translate(0.0, edge);
        }

        poly.matrixTransform(aTransform);
        res.appendPath(poly);
    }
    else { //out
        const sqrtArea2 = Math.sqrt(nT * this.sqrtElements * this.sqrtElements);
        const edge = pruneScaleValue(Math.floor(sqrtArea2)/this.sqrtElements);

        let aPath, aPoint = document.documentElement.createSVGPoint();
        if(edge != 0) {
            aPoint.y = 1.0;
            aPath = 'M ' + aPoint.x + ' ' + aPoint.y + ' ';
            aPoint.x = edge;
            aPath += 'L ' + aPoint.x + ' ' + aPoint.y + ' ';
            aPoint.x = 1.0;
            aPoint.y = edge;
            aPath += 'L ' + aPoint.x + ' ' + aPoint.y + ' ';
            aPoint.y = 0.0;
            aPath += 'L ' + aPoint.x + ' ' + aPoint.y + ' ';
            aPoint.x = 0.0;
            aPath += 'L ' + aPoint.x + ' ' + aPoint.y + ' ';
            const poly = document.createElementNS( NSS['svg'], 'path');
            poly.setAttribute('d', aPath);
            res.appendPath(poly);
        }
        const a = (Math.SQRT1_2 / this.sqrtElements);
        const d = (sqrtArea2 - Math.floor(sqrtArea2));
        const len = ((1.0 - nT) * Math.SQRT2 * d);
        const height = pruneScaleValue(Math.SQRT1_2 / this.sqrtElements);
        aPath = 'M ' + aPoint.x + ' ' + aPoint.y + ' ';
        aPoint.y = height;
        aPath += 'L ' + aPoint.x + ' ' + aPoint.y + ' ';
        aPoint.x = len + a;
        aPath += 'L ' + aPoint.x + ' ' + aPoint.y + ' ';
        aPoint.y = 0.0;
        aPath += 'L ' + aPoint.x + ' ' + aPoint.y + ' ';
        aPoint.x = 0.0;
        aPath += 'L ' + aPoint.x + ' ' + aPoint.y + ' ';
        const poly = document.createElementNS( NSS['svg'], 'path');
        poly.setAttribute('d', aPath);
        let aTransform;

        if((Math.floor(sqrtArea2) & 1) == 1) {
            // odd line
            aTransform = SVGIdentityMatrix.translate(0.0, -height);
            aTransform.rotate(Math.PI/2 + Math.PI/4);
            aTransform.translate(1.0, edge);
        }
        else {
            aTransform = SVGIdentityMatrix.rotate(-(Math.PI/4));
            aTransform = aTransform.translate(edge, 1.0);
        }
        poly.matrixTransform(aTransform);
        res.appendPath(poly);
    }
    return res;
}

SnakeWipePath.prototype.perform = function(nT) {
    var res = createEmptyPath();
    if(this.diagonal) {
        if(nT >= 0.5) {
            res.appendPath(this.calcHalfDiagonalSnake(1.0, true));
            res.appendPath(this.calcHalfDiagonalSnake(2.0*(nT-0.5), false));
        }
        else
            res.appendPath(this.calcHalfDiagonalSnake(2.0*nT, true));
    }
    else
        res = this.calcSnake(nT);

    return this.flipOnYAxis ? flipOnYAxis(res) : res;
}

/** Class ParallelSnakesWipePath
 *  Generates a parallel snakes wipe:
 *
 *  @param nElements
 *  @param bDiagonal
 *  @param bFlipOnYAxis
 *  @param bOpposite
 */
function ParallelSnakesWipePath(nElements, bDiagonal, bFlipOnYAxis, bOpposite) {
    SnakeWipePath.call(this, nElements, bDiagonal, bFlipOnYAxis);
    this.bOpposite = bOpposite;
}

ParallelSnakesWipePath.prototype = Object.create(SnakeWipePath);

ParallelSnakesWipePath.prototype.perform = function( nT ) {
    var res = createEmptyPath(), half, aTransform;
    if(this.diagonal) {
        assert(this.bOpposite);
        half = SnakeWipePath.prototype.calcHalfDiagonalSnake.call(this, nT, false);
        // flip on x axis and rotate 90 degrees:
        aTransform = SVGIdentityMatrix.scale(1, -1);
        aTransform.translate(-0.5, 0.5);
        aTransform.rotate(Math.PI/2);
        aTransform.translate(0.5, 0.5);
        half.matrixTransform(aTransform);
        half.changeOrientation();
        res.appendPath(half);

        // rotate 180 degrees:
        aTransform = SVGIdentityMatrix.translate(-0.5, -0.5);
        aTransform.rotate(Math.PI);
        aTransform.translate(0.5, 0.5);
        half.matrixTransform(aTransform);
        res.appendPath(half);
    }
    else {
        half = SnakeWipePath.prototype.calcSnake.call(this, nT / 2.0 );
        // rotate 90 degrees
        aTransform = SVGIdentityMatrix.translate(-0.5, -0.5);
        aTransform = aTransform.rotate(Math.PI/2);
        aTransform = aTransform.translate(0.5, 0.5);
        half.matrixTransform(aTransform);
        res.appendPath(flipOnYAxis(half));
        res.appendPath(this.bOpposite ? flipOnXAxis(half) : half);
    }

    return this.flipOnYAxis ? flipOnYAxis(res) : res;
}

/** SpiralWipePath
 *
 *  @param nElements
 *      number of elements in the spiral animation
 *  @param bFlipOnYAxis
 *      boolean value indicating whether to flip on y-axis or not.
 */
function SpiralWipePath(nElements, bFlipOnYAxis) {
    this.nElements    = nElements;
    this.sqrtElements = Math.floor(Math.sqrt(nElements));
    this.bFlipOnYAxis = bFlipOnYAxis;
}

SpiralWipePath.prototype.calcNegSpiral = function( nT ) {
    var area  = nT * this.nElements;
    var e     = (Math.sqrt(area) / 2.0);
    var edge  = Math.floor(e) * 2;

    var aTransform = SVGIdentityMatrix.translate(-0.5, -0.5);
    var edge_ = pruneScaleValue(edge / this.sqrtElements);

    aTransform = aTransform.scale(edge_, edge_);
    aTransform = aTransform.translate(0.5, 0.5);
    var poly = createUnitSquarePath();
    poly.matrixTransform(aTransform);
    var res = poly.cloneNode(true);

    if(1.0 - nT != 0) {
        var edge1 = edge + 1;
        var len   = Math.floor( (e - edge/2) * edge1 * 4);
        var w     = Math.PI / 2;

        while(len > 0) {
            var alen = Math.min(len, edge1);
            len -= alen;
            poly = createUnitSquarePath();
            aTransform = SVGIdentityMatrix.scale(
                            pruneScaleValue( alen / this.sqrtElements ),
                            pruneScaleValue( 1.0 / this.sqrtElements ));
            aTransform = aTransform.translate(
                            - pruneScaleValue( (edge / 2) / this.sqrtElements ),
                            pruneScaleValue( (edge / 2) / this.sqrtElements ));
            aTransform = aTransform.rotate( w );
            w -= Math.PI / 2;
            aTransform = aTransform.translate(0.5, 0.5);
            poly.matrixTransform(aTransform);
            res.appendPath(poly);
        }
    }

    return res;
}

SpiralWipePath.prototype.perform = function( nT ) {
    var res         = createUnitSquarePath();
    var innerSpiral = this.calcNegSpiral( 1.0 - nT );
    innerSpiral.changeOrientation();
    res.appendPath(innerSpiral);

    return this.bFlipOnYAxis ? flipOnYAxis(res) : res;
}

/** Class BoxSnakesWipePath
 *  Generates a twoBoxLeft or fourBoxHorizontal wipe:
 *
 */
function BoxSnakesWipePath(nElements, bFourBox) {
    SpiralWipePath.call(this, nElements);
    this.bFourBox = bFourBox;
}

BoxSnakesWipePath.prototype = Object.create(SpiralWipePath);

BoxSnakesWipePath.prototype.perform = function( nT ) {
    var res = createUnitSquarePath(), aTransform;
    var innerSpiral = SpiralWipePath.prototype.calcNegSpiral.call(this, 1.0 - nT);
    innerSpiral.changeOrientation();

    if(this.bFourBox) {
        aTransform = SVGIdentityMatrix.scale(0.5, 0.5);
        innerSpiral.matrixTransform(aTransform);
        res.appendPath(innerSpiral);
        res.appendPath(flipOnXAxis(innerSpiral));
        innerSpiral = flipOnYAxis(innerSpiral);
        res.appendPath(innerSpiral);
        res.appendPath(flipOnXAxis(innerSpiral));
    }
    else {
        aTransform = SVGIdentityMatrix.scale(1.0, 0.5);
        innerSpiral.matrixTransform(aTransform);
        res.appendPath(innerSpiral);
        res.appendPath(flipOnXAxis(innerSpiral));
    }
    return this.bFlipOnYAxis ? flipOnYAxis(res) : res;
}

/** Class VeeWipePath
  *
  */
function VeeWipePath() { }

VeeWipePath.prototype.perform = function( nT ) {
    const d = pruneScaleValue(2.0 * nT);
    var polyPath = 'M ' + 0.0 + ' ' + -1.0 + ' ';
    polyPath += 'L ' + 0.0 + ' ' + (d - 1.0) + ' ';
    polyPath += 'L ' + 0.5 + ' ' + d + ' ';
    polyPath += 'L ' + 1.0 + ' ' + (d - 1.0) + ' ';
    polyPath += 'L ' + 1.0 + ' ' + -1.0 + ' ';
    polyPath += 'L ' + 0.0 + ' ' + -1.0 + ' ';

    var aPolyPolyPath = document.createElementNS( NSS['svg'], 'path');
    aPolyPolyPath.setAttribute('d', polyPath);
    return aPolyPolyPath;
}


/** Class AnimatedSlide
 *  This class handle a slide element during a slide transition.
 *
 *  @param aMetaSlide
 *      The MetaSlide object related to the slide element to be handled.
 */
function AnimatedSlide( aMetaSlide )
{
    if( !aMetaSlide )
    {
        log( 'AnimatedSlide constructor: meta slide is not valid' );
    }

    this.aMetaSlide = aMetaSlide;
    this.aSlideElement = this.aMetaSlide.slideElement;
    this.sSlideId = this.aMetaSlide.slideId;

    this.aUsedAttributeSet = [];

    this.aClipPathElement = null;
    this.aClipPathContent = null;
    this.bIsClipped = false;
}

/** show
 *  Set the visibility property of the slide to 'inherit'
 *  and update the master page view.
 */
AnimatedSlide.prototype.show = function()
{
    this.aMetaSlide.show();
};

/** hide
 *  Set the visibility property of the slide to 'hidden'.
 */
AnimatedSlide.prototype.hide = function()
{
    this.aMetaSlide.hide();
};

/** notifyUsedAttribute
 *  Populate the set of attribute used for the transition.
 *
 *  @param sName
 *      A string representing an attribute name.
 */
AnimatedSlide.prototype.notifyUsedAttribute = function( sName )
{
    if( sName == 'clip-path' )
    {
        this.initClipPath();
        this.bIsClipped = true;
    }
    else
    {
        this.aUsedAttributeSet.push( sName );
    }
};

/** reset
 *  Remove from the handled slide element any attribute that was appended for
 *  performing the transition.
 */
AnimatedSlide.prototype.reset = function()
{
    if( this.bIsClipped )
    {
        this.cleanClipPath();
        this.bIsClipped = false;
    }

    var i;
    for( i = 0; i < this.aUsedAttributeSet.length; ++i )
    {
        var sAttrName = this.aUsedAttributeSet[i];
        this.aSlideElement.removeAttribute( sAttrName );
    }
    this.aUsedAttributeSet = [];
};

/** initClipPath
 *  Create a new clip path element and append it to the clip path def group.
 *  Moreover the created <clipPath> element is referenced by the handled slide
 *  element.
 */
AnimatedSlide.prototype.initClipPath = function()
{
    // We create the clip path element.
    this.aClipPathElement = document.createElementNS( NSS['svg'], 'clipPath' );

    var sId = 'clip-path-' + this.sSlideId;
    this.aClipPathElement.setAttribute( 'id', sId );
    this.aClipPathElement.setAttribute( 'clipPathUnits', 'userSpaceOnUse' );

    // We create and append a placeholder content.
    this.aClipPathContent = document.createElementNS( NSS['svg'], 'path' );
    var sPathData = 'M 0 0 h ' + WIDTH + ' v ' + HEIGHT + ' h -' + WIDTH + ' z';
    this.aClipPathContent.setAttribute( 'd', sPathData );
    this.aClipPathElement.appendChild( this.aClipPathContent );

    // We insert it into the svg document.
    var aClipPathGroup = theMetaDoc.aClipPathGroup;
    aClipPathGroup.appendChild( this.aClipPathElement );

    // Finally we set the reference to the created clip path.
    // We set it on the parent element because a slide element already
    // owns a clip path attribute.
    var sRef = 'url(#' + sId + ')';
    this.aSlideElement.parentNode.setAttribute( 'clip-path', sRef );
};

/** cleanClipPath
 *  Removes the related <clipPath> element from the <defs> group,
 *  and remove the 'clip-path' attribute from the slide element.
 *
 */
AnimatedSlide.prototype.cleanClipPath = function()
{
    this.aSlideElement.parentNode.removeAttribute( 'clip-path' );

    if( this.aClipPathElement )
    {
        var aClipPathGroup = theMetaDoc.aClipPathGroup;
        aClipPathGroup.removeChild( this.aClipPathElement );
        this.aClipPathElement = null;
        this.aClipPathContent = null;
    }
};

/** insertBefore
 *  Insert an svg element before the handled slide element.
 *
 *  @param aElement
 *      A svg element.
 */
AnimatedSlide.prototype.insertBefore = function( aElement )
{
    if( aElement )
    {
        this.aSlideElement.parentNode.insertBefore( aElement, this.aSlideElement );
    }
};

/** appendElement
 *  Insert an svg element after the handled slide element.
 *
 *  @param aElement
 *      A svg element.
 */
AnimatedSlide.prototype.appendElement = function( aElement )
{
    if( aElement )
    {
         this.aSlideElement.parentNode.appendChild( aElement );
    }
};

/** removeElement
 *  Remove an svg element.
 *
 *  @param aElement
 *      A svg element.
 */
AnimatedSlide.prototype.removeElement = function( aElement )
{
    if( aElement )
    {
        this.aSlideElement.parentNode.removeChild( aElement );
    }
};

/** getWidth
 *
 *  @return {Number}
 *      The slide width.
 */
AnimatedSlide.prototype.getWidth = function()
{
    return WIDTH;
};

/** getHeight
 *
 *  @return {Number}
 *      The slide height.
 */
AnimatedSlide.prototype.getHeight = function()
{
    return HEIGHT;
};

/** setOpacity
 *
 *  @param nValue
 *      A number in the [0,1] range representing the slide opacity.
 */
AnimatedSlide.prototype.setOpacity = function( nValue )
{
    this.aSlideElement.setAttribute( 'opacity', nValue );
};

/** translate
 *  Translate the handled slide.
 *
 *  @param nDx
 *     A number representing the translation that occurs in the x direction.
 *  @param nDy
 *     A number representing the translation that occurs in the y direction.
 */
AnimatedSlide.prototype.translate = function( nDx, nDy )
{
    var sTransformAttr = 'translate(' + nDx + ',' + nDy + ')';
    this.aSlideElement.setAttribute( 'transform', sTransformAttr );
};

/** setClipPath
 *  Replace the current content of the <clipPath> element with the one
 *  passed through the parameter.
 *
 *  @param aClipPathContent
 *      A <g> element representing a <path> element used for clipping.
 */
AnimatedSlide.prototype.setClipPath = function( aClipPathContent )
{
    // Earlier we used to replace the current <path> element with the passed one,
    // anyway that does not work in IE9, so we replace the 'd' attribute, only.
    if( this.aClipPathContent )
    {
        var sPathData = aClipPathContent.getAttribute( 'd' );
        this.aClipPathContent.setAttribute( 'd', sPathData );
    }
};



function AnimatedElement( aElement )
{
    if( !aElement )
    {
        log( 'AnimatedElement constructor: element is not valid' );
    }

    this.aSlideShowContext = null;

    this.aBaseElement = aElement.cloneNode( true );
    this.aActiveElement = aElement;
    this.sElementId = this.aActiveElement.getAttribute( 'id' );

    this.aBaseBBox = this.aActiveElement.getBBox();
    this.nBaseCenterX = this.aBaseBBox.x + this.aBaseBBox.width / 2;
    this.nBaseCenterY = this.aBaseBBox.y + this.aBaseBBox.height / 2;


    this.aClipPathElement = null;
    this.aClipPathContent = null;

    this.aPreviousElement = null;
    this.aStateSet = {};

    this.eAdditiveMode = ADDITIVE_MODE_REPLACE;
    this.bIsUpdated = true;

    this.aTMatrix = document.documentElement.createSVGMatrix();
    this.aCTM = document.documentElement.createSVGMatrix();
    this.aICTM = document.documentElement.createSVGMatrix();

    this.initElement();
}

AnimatedElement.prototype.initElement = function()
{
    this.nCenterX = this.nBaseCenterX;
    this.nCenterY = this.nBaseCenterY;
    this.nScaleFactorX = 1.0;
    this.nScaleFactorY = 1.0;
    this.nRotationAngle = 0.0;

    // add a transform attribute of type matrix
    this.aActiveElement.setAttribute( 'transform', makeMatrixString( 1, 0, 0, 1, 0, 0 ) );
};

/** initClipPath
 *  Create a new clip path element and append it to the clip path def group.
 *  Moreover the created <clipPath> element is referenced by the handled
 *  animated element.
 *
 */
AnimatedElement.prototype.initClipPath = function()
{
    // We create the clip path element.
    this.aClipPathElement = document.createElementNS( NSS['svg'], 'clipPath' );

    var sId = 'clip-path-' + this.sElementId;
    this.aClipPathElement.setAttribute( 'id', sId );
    this.aClipPathElement.setAttribute( 'clipPathUnits', 'userSpaceOnUse' );

    // We create and append a placeholder content.
    this.aClipPathContent = document.createElementNS( NSS['svg'], 'path' );
    this.aClippingBBox = this.getBBoxWithStroke();
    var nWidth = this.aClippingBBox.width;
    var nHeight = this.aClippingBBox.height;
    var sPathData = 'M ' + this.aClippingBBox.x + ' ' + this.aClippingBBox.y +
                    ' h ' + nWidth + ' v ' + nHeight + ' h -' + nWidth + ' z';
    this.aClipPathContent.setAttribute( 'd', sPathData );
    this.aClipPathElement.appendChild( this.aClipPathContent );

    // We insert it into the svg document.
    var aClipPathGroup = theMetaDoc.aClipPathGroup;
    aClipPathGroup.appendChild( this.aClipPathElement );

    // Finally we set the reference to the created clip path.
    var sRef = 'url(#' + sId + ')';
    this.aActiveElement.setAttribute( 'clip-path', sRef );
};

/** cleanClipPath
 *  Removes the related <clipPath> element from the <defs> group,
 *  and remove the 'clip-path' attribute from the animated element.
 *
 */
AnimatedElement.prototype.cleanClipPath = function()
{
    this.aActiveElement.removeAttribute( 'clip-path' );

    if( this.aClipPathElement )
    {
        var aClipPathGroup = theMetaDoc.aClipPathGroup;
        aClipPathGroup.removeChild( this.aClipPathElement );
        this.aClipPathElement = null;
        this.aClipPathContent = null;
    }
};

AnimatedElement.prototype.getId = function()
{
    return this.aActiveElement.getAttribute( 'id' );
};

AnimatedElement.prototype.getAdditiveMode = function()
{
    return this.eAdditiveMode;
};

AnimatedElement.prototype.setAdditiveMode = function( eAdditiveMode )
{
    this.eAdditiveMode = eAdditiveMode;
};

AnimatedElement.prototype.setToElement = function( aElement )
{
    if( !aElement )
    {
        log( 'AnimatedElement(' + this.getId() + ').setToElement: element is not valid' );
        return false;
    }

    var aClone = aElement.cloneNode( true );
    this.aPreviousElement = this.aActiveElement.parentNode.replaceChild( aClone, this.aActiveElement );
    this.aActiveElement = aClone;

    return true;
};

AnimatedElement.prototype.notifySlideStart = function( aSlideShowContext )
{
    if( !aSlideShowContext )
    {
        log( 'AnimatedElement.notifySlideStart: slideshow context is not valid' );
    }
    this.aSlideShowContext = aSlideShowContext;

    var aClone = this.aBaseElement.cloneNode( true );
    this.aActiveElement.parentNode.replaceChild( aClone, this.aActiveElement );
    this.aActiveElement = aClone;

    this.initElement();
    this.DBG( '.notifySlideStart invoked' );
};

AnimatedElement.prototype.notifySlideEnd = function()
{
    // empty body
};

AnimatedElement.prototype.notifyAnimationStart = function()
{
    // empty body
};

AnimatedElement.prototype.notifyAnimationEnd = function()
{
    // empty body
};

AnimatedElement.prototype.notifyNextEffectStart = function( /*nEffectIndex*/ )
{
    // empty body
};

/** saveState
 *  Save the state of the managed animated element and append it to aStateSet
 *  using the passed animation node id as key.
 *
 *  @param nAnimationNodeId
 *      A non negative integer representing the unique id of an animation node.
 */
AnimatedElement.prototype.saveState = function( nAnimationNodeId )
{
    ANIMDBG.print( 'AnimatedElement(' + this.getId() + ').saveState(' + nAnimationNodeId +')' );
    if( !this.aStateSet[ nAnimationNodeId ] )
    {
        this.aStateSet[ nAnimationNodeId ] = {};
    }
    var aState = this.aStateSet[ nAnimationNodeId ];
    aState.aElement = this.aActiveElement.cloneNode( true );
    aState.nCenterX = this.nCenterX;
    aState.nCenterY = this.nCenterY;
    aState.nScaleFactorX = this.nScaleFactorX;
    aState.nScaleFactorY = this.nScaleFactorY;
    aState.nRotationAngle = this.nRotationAngle;

};

/** restoreState
 *  Restore the state of the managed animated element to the state with key
 *  the passed animation node id.
 *
 *  @param nAnimationNodeId
 *      A non negative integer representing the unique id of an animation node.
 *
 *  @return
 *      True if the restoring operation is successful, false otherwise.
 */
AnimatedElement.prototype.restoreState = function( nAnimationNodeId )
{
    if( !this.aStateSet[ nAnimationNodeId ] )
    {
        log( 'AnimatedElement(' + this.getId() + ').restoreState: state '
                 +nAnimationNodeId  + ' is not valid' );
        return false;
    }

    ANIMDBG.print( 'AnimatedElement(' + this.getId() + ').restoreState(' + nAnimationNodeId +')' );
    var aState = this.aStateSet[ nAnimationNodeId ];
    var bRet = this.setToElement( aState.aElement );
    if( bRet )
    {
        this.nCenterX = aState.nCenterX;
        this.nCenterY = aState.nCenterY;
        this.nScaleFactorX = aState.nScaleFactorX;
        this.nScaleFactorY = aState.nScaleFactorY;
        this.nRotationAngle = aState.nRotationAngle;
    }
    return bRet;
};

AnimatedElement.prototype.getBaseBBox = function()
{
    return this.aBaseBBox;
};

AnimatedElement.prototype.getBaseCenterX = function()
{
    return this.nBaseCenterX;
};

AnimatedElement.prototype.getBaseCenterY = function()
{
    return this.nBaseCenterY;
};

AnimatedElement.prototype.getBBox = function()
{
    return this.aActiveElement.parentNode.getBBox();
};

AnimatedElement.prototype.getBBoxWithStroke = function()
{
    var aBBox = this.aActiveElement.parentNode.getBBox();

    var aChildrenSet = this.aActiveElement.childNodes;

    var sStroke, sStrokeWidth;
    var nStrokeWidth = 0;
    var i;
    for( i = 0; i < aChildrenSet.length; ++i )
    {
        if( ! aChildrenSet[i].getAttribute  )
            continue;

        sStroke = aChildrenSet[i].getAttribute( 'stroke' );
        if( sStroke && sStroke != 'none' )
        {
            sStrokeWidth = aChildrenSet[i].getAttribute( 'stroke-width' );
            var nSW = parseFloat( sStrokeWidth );
            if( nSW > nStrokeWidth )
                nStrokeWidth = nSW;
        }
    }

    if( nStrokeWidth == 0 )
    {
        sStrokeWidth = ROOT_NODE.getAttribute( 'stroke-width' );
        nStrokeWidth = parseFloat( sStrokeWidth );
    }
    if( nStrokeWidth != 0 )
    {
        // It is hard to clip properly the stroke so we try to enlarge
        // the resulting bounding box even more.
        nStrokeWidth *= 1.1;
        var nHalfStrokeWidth = nStrokeWidth / 2;
        var nDoubleStrokeWidth = nStrokeWidth * 2;

        // Note: IE10 don't let modify the values of an element BBox.
        var aEBBox = document.documentElement.createSVGRect();
        aEBBox.x = aBBox.x - nHalfStrokeWidth;
        aEBBox.y = aBBox.y - nHalfStrokeWidth;
        aEBBox.width = aBBox.width + nDoubleStrokeWidth;
        aEBBox.height = aBBox.height + nDoubleStrokeWidth;
        aBBox = aEBBox;
    }
    return aBBox;
};

/** setClipPath
 *  Replace the current content of the <clipPath> element with the one
 *  passed through the parameter.
 *
 *  @param aClipPathContent
 *      A <g> element representing a <path> element used for clipping.
 */
AnimatedElement.prototype.setClipPath = function( aClipPathContent )
{
    if( this.aClipPathContent )
    {
        // We need to translate the clip path to the top left corner of
        // the element bounding box.
        var aTranslation = SVGIdentityMatrix.translate( this.aClippingBBox.x,
                                                        this.aClippingBBox.y);
        aClipPathContent.matrixTransform( aTranslation );
        var sPathData = aClipPathContent.getAttribute( 'd' );
        this.aClipPathContent.setAttribute( 'd', sPathData );
    }
};


AnimatedElement.prototype.getX = function()
{
    return this.nCenterX;
};

AnimatedElement.prototype.getY = function()
{
    return this.nCenterY;
};

AnimatedElement.prototype.getPos = function()
{
    return [this.getX(), this.getY()];
};

AnimatedElement.prototype.getWidth = function()
{
    return this.nScaleFactorX * this.getBaseBBox().width;
};

AnimatedElement.prototype.getHeight = function()
{
    return this.nScaleFactorY * this.getBaseBBox().height;
};

AnimatedElement.prototype.getSize = function()
{
    return [this.getWidth(), this.getHeight()];
};

AnimatedElement.prototype.updateTransformAttribute = function()
{
    this.aTransformAttrList = this.aActiveElement.transform.baseVal;
    this.aTransformAttr = this.aTransformAttrList.getItem( 0 );
    this.aTransformAttr.setMatrix( this.aTMatrix );
};

AnimatedElement.prototype.setX = function( nNewCenterX )
{
    if( nNewCenterX === this.nCenterX ) return;

    this.aTransformAttrList = this.aActiveElement.transform.baseVal;
    this.aTransformAttr = this.aTransformAttrList.getItem( 0 );
    this.aTMatrix = this.aTransformAttr.matrix.translate( nNewCenterX - this.nCenterX, 0 );
    this.aTransformAttr.setMatrix( this.aTMatrix );
    this.nCenterX = nNewCenterX;
};

AnimatedElement.prototype.setY = function( nNewCenterY )
{
    if( nNewCenterY === this.nCenterY ) return;

    this.aTransformAttrList = this.aActiveElement.transform.baseVal;
    this.aTransformAttr = this.aTransformAttrList.getItem( 0 );
    this.aTMatrix = this.aTransformAttr.matrix.translate( 0, nNewCenterY - this.nCenterY );
    this.aTransformAttr.setMatrix( this.aTMatrix );
    this.nCenterY = nNewCenterY;
};

AnimatedElement.prototype.setPos = function( aNewPos )
{
    var nNewCenterX = aNewPos[0];
    var nNewCenterY = aNewPos[1];

    if( nNewCenterX === this.nCenterX && nNewCenterY === this.nCenterY ) return;

    this.aTransformAttrList = this.aActiveElement.transform.baseVal;
    this.aTransformAttr = this.aTransformAttrList.getItem( 0 );
    this.aTMatrix = this.aTransformAttr.matrix.translate( nNewCenterX - this.nCenterX, nNewCenterY - this.nCenterY );
    this.aTransformAttr.setMatrix( this.aTMatrix );
    this.nCenterX = nNewCenterX;
    this.nCenterY = nNewCenterY;
};

AnimatedElement.prototype.setWidth = function( nNewWidth )
{
    ANIMDBG.print( 'AnimatedElement.setWidth: nNewWidth = ' + nNewWidth );
    if( nNewWidth < 0 )
    {
        log('AnimatedElement(' + this.getId() + ').setWidth: negative width!');
        nNewWidth = 0;
    }

    var nBaseWidth = this.getBaseBBox().width;
    var nScaleFactorX = nNewWidth / nBaseWidth;

    if( nScaleFactorX < 1e-5 ) nScaleFactorX = 1e-5;
    if( nScaleFactorX == this.nScaleFactorX ) return;

    this.aTMatrix = document.documentElement.createSVGMatrix()
        .translate( this.nCenterX, this.nCenterY )
        .rotate(this.nRotationAngle)
        .scaleNonUniform( nScaleFactorX, this.nScaleFactorY )
        .translate( -this.nBaseCenterX, -this.nBaseCenterY );
    this.updateTransformAttribute();

    this.nScaleFactorX = nScaleFactorX;
};

AnimatedElement.prototype.setHeight = function( nNewHeight )
{
    ANIMDBG.print( 'AnimatedElement.setWidth: nNewHeight = ' + nNewHeight );
    if( nNewHeight < 0 )
    {
        log('AnimatedElement(' + this.getId() + ').setWidth: negative height!');
        nNewHeight = 0;
    }

    var nBaseHeight = this.getBaseBBox().height;
    var nScaleFactorY = nNewHeight / nBaseHeight;

    if( nScaleFactorY < 1e-5 ) nScaleFactorY = 1e-5;
    if( nScaleFactorY == this.nScaleFactorY ) return;

    this.aTMatrix = document.documentElement.createSVGMatrix()
        .translate( this.nCenterX, this.nCenterY )
        .rotate(this.nRotationAngle)
        .scaleNonUniform( this.nScaleFactorX, nScaleFactorY )
        .translate( -this.nBaseCenterX, -this.nBaseCenterY );
    this.updateTransformAttribute();

    this.nScaleFactorY = nScaleFactorY;
};

AnimatedElement.prototype.setSize= function( aNewSize )
{
    var nNewWidth = aNewSize[0];
    var nNewHeight = aNewSize[1];
    ANIMDBG.print( 'AnimatedElement.setSize:  = [' + nNewWidth + ',' + nNewHeight + ']');
    if( nNewWidth < 0 )
    {
        log('AnimatedElement(' + this.getId() + ').setSize: negative width!');
        nNewWidth = 0;
    }
    if( nNewHeight < 0 )
    {
        log('AnimatedElement(' + this.getId() + ').setSize: negative height!');
        nNewHeight = 0;
    }

    var nBaseWidth = this.getBaseBBox().width;
    var nScaleFactorX = nNewWidth / nBaseWidth;
    if( nScaleFactorX < 1e-5 ) nScaleFactorX = 1e-5;

    var nBaseHeight = this.getBaseBBox().height;
    var nScaleFactorY = nNewHeight / nBaseHeight;
    if( nScaleFactorY < 1e-5 ) nScaleFactorY = 1e-5;

    if( nScaleFactorX == this.nScaleFactorX && nScaleFactorY == this.nScaleFactorY ) return;

    this.aTMatrix = document.documentElement.createSVGMatrix()
        .translate( this.nCenterX, this.nCenterY )
        .rotate(this.nRotationAngle)
        .scaleNonUniform( nScaleFactorX, nScaleFactorY )
        .translate( -this.nBaseCenterX, -this.nBaseCenterY );
    this.updateTransformAttribute();

    this.nScaleFactorX = nScaleFactorX;
    this.nScaleFactorY = nScaleFactorY;
};

AnimatedElement.prototype.getOpacity = function()
{
    return this.aActiveElement.getAttribute( 'opacity' );
};

AnimatedElement.prototype.setOpacity = function( nValue )
{
    this.aActiveElement.setAttribute( 'opacity', nValue );
};

AnimatedElement.prototype.getRotationAngle = function()
{
    return this.nRotationAngle;
};

AnimatedElement.prototype.setRotationAngle = function( nNewRotAngle )
{
    this.aTMatrix = document.documentElement.createSVGMatrix()
        .translate( this.nCenterX, this.nCenterY )
        .rotate(nNewRotAngle)
        .scaleNonUniform( this.nScaleFactorX, this.nScaleFactorY )
        .translate( -this.nBaseCenterX, -this.nBaseCenterY );
    this.updateTransformAttribute();

    this.nRotationAngle = nNewRotAngle;
};

AnimatedElement.prototype.getVisibility = function()
{

    var sVisibilityValue = this.aActiveElement.getAttribute( 'visibility' );
    if( !sVisibilityValue || ( sVisibilityValue === 'inherit' ) )
        return 'visible'; // TODO: look for parent visibility!
    else
        return sVisibilityValue;
};

AnimatedElement.prototype.setVisibility = function( sValue )
{
    if( sValue == 'visible' )
        sValue = 'inherit';
    this.aActiveElement.setAttribute( 'visibility', sValue );
};

AnimatedElement.prototype.getStrokeStyle = function()
{
    // TODO: getStrokeStyle: implement it
    return 'solid';
};

AnimatedElement.prototype.setStrokeStyle = function( sValue )
{
    ANIMDBG.print( 'AnimatedElement.setStrokeStyle(' + sValue + ')' );
};

AnimatedElement.prototype.getFillStyle = function()
{
    // TODO: getFillStyle: implement it
    return 'solid';
};

AnimatedElement.prototype.setFillStyle = function( sValue )
{
    ANIMDBG.print( 'AnimatedElement.setFillStyle(' + sValue + ')' );
};

AnimatedElement.prototype.getFillColor = function()
{
    var aChildSet = getElementChildren( this.aActiveElement );
    var sFillColorValue = '';
    for( var i = 0; i <  aChildSet.length; ++i )
    {
        sFillColorValue = aChildSet[i].getAttribute( 'fill' );
        if( sFillColorValue && ( sFillColorValue !== 'none' ) )
            break;
    }

    return colorParser( sFillColorValue );
};

AnimatedElement.prototype.setFillColor = function( aRGBValue )
{
    assert( aRGBValue instanceof RGBColor,
            'AnimatedElement.setFillColor: value argument is not an instance of RGBColor' );

    var sValue = aRGBValue.toString( true /* clamped values */ );
    var aChildSet = getElementChildren( this.aActiveElement );

    var sFillColorValue = '';
    for( var i = 0; i <  aChildSet.length; ++i )
    {
        sFillColorValue = aChildSet[i].getAttribute( 'fill' );
        if( sFillColorValue && ( sFillColorValue !== 'none' ) )
        {
            aChildSet[i].setAttribute( 'fill', sValue );
        }
    }
};

AnimatedElement.prototype.getStrokeColor = function()
{
    var aChildSet = getElementChildren( this.aActiveElement );
    var sStrokeColorValue = '';
    for( var i = 0; i <  aChildSet.length; ++i )
    {
        sStrokeColorValue = aChildSet[i].getAttribute( 'stroke' );
        if( sStrokeColorValue && ( sStrokeColorValue !== 'none' ) )
            break;
    }

    return colorParser( sStrokeColorValue );
};

AnimatedElement.prototype.setStrokeColor = function( aRGBValue )
{
    assert( aRGBValue instanceof RGBColor,
            'AnimatedElement.setFillColor: value argument is not an instance of RGBColor' );

    var sValue = aRGBValue.toString( true /* clamped values */ );
    var aChildSet = getElementChildren( this.aActiveElement );

    var sStrokeColorValue = '';
    for( var i = 0; i <  aChildSet.length; ++i )
    {
        sStrokeColorValue = aChildSet[i].getAttribute( 'stroke' );
        if( sStrokeColorValue && ( sStrokeColorValue !== 'none' ) )
        {
            aChildSet[i].setAttribute( 'stroke', sValue );
        }
    }
};

AnimatedElement.prototype.getFontColor = function()
{
    // TODO: getFontColor implement it
    return new RGBColor( 0, 0, 0 );
};

AnimatedElement.prototype.setFontColor = function( sValue )
{
    ANIMDBG.print( 'AnimatedElement.setFontColor(' + sValue + ')' );
};

AnimatedElement.prototype.DBG = function( sMessage, nTime )
{
    aAnimatedElementDebugPrinter.print( 'AnimatedElement(' + this.getId() + ')' + sMessage, nTime );
};


function AnimatedTextElement( aElement, aEventMultiplexer )
{
    var theDocument = document;

    var sTextType = aElement.getAttribute( 'class' );
    var bIsListItem = ( sTextType === 'ListItem' );
    if( ( sTextType !== 'TextParagraph' ) && !bIsListItem )
    {
        log( 'AnimatedTextElement: passed element is not a paragraph.' );
        return;
    }
    var aTextShapeElement = aElement.parentNode;
    sTextType = aTextShapeElement.getAttribute( 'class' );
    if( sTextType !== 'SVGTextShape' )
    {
        log( 'AnimatedTextElement: element parent is not a text shape.' );
        return;
    }
    var aTextShapeGroup = aTextShapeElement.parentNode;
    // We search for the helper group element used for inserting
    // the element copy to be animated; if it doesn't exist we create it.
    var aAnimatedElementGroup = getElementByClassName( aTextShapeGroup, 'AnimatedElements' );
    if( !aAnimatedElementGroup )
    {
        aAnimatedElementGroup = theDocument.createElementNS( NSS['svg'], 'g' );
        aAnimatedElementGroup.setAttribute( 'class', 'AnimatedElements' );
        aTextShapeGroup.appendChild( aAnimatedElementGroup );
    }

    // Create element used on animating
    var aAnimatableElement = theDocument.createElementNS( NSS['svg'], 'g' );
    var aTextElement = theDocument.createElementNS( NSS['svg'], 'text' );
    // Clone paragraph element <tspan>
    var aParagraphElement = aElement.cloneNode( true );

    // We create a group element for wrapping bullets, bitmaps
    // and text decoration
    this.aGraphicGroupElement = theDocument.createElementNS( NSS['svg'], 'g' );
    this.aGraphicGroupElement.setAttribute( 'class', 'GraphicGroup' );

    // In case we are dealing with a list item that utilizes a bullet char
    // we need to clone the related bullet char too.
    var aBulletCharClone = null;
    var aBulletCharElem = null;
    var bIsBulletCharStyle =
        ( aElement.getAttributeNS( NSS['ooo'], aOOOAttrListItemNumberingType ) === 'bullet-style' );
    if( bIsBulletCharStyle )
    {
        var aBulletCharGroupElem = getElementByClassName( aTextShapeGroup, 'BulletChars' );
        if( aBulletCharGroupElem )
        {
            var aBulletPlaceholderElem = getElementByClassName( aElement, 'BulletPlaceholder' );
            if( aBulletPlaceholderElem )
            {
                var sId = aBulletPlaceholderElem.getAttribute( 'id' );
                sId = 'bullet-char-' + sId;
                aBulletCharElem = theDocument.getElementById( sId );
                if( aBulletCharElem )
                {
                    aBulletCharClone = aBulletCharElem.cloneNode( true );
                }
                else
                {
                    log( 'AnimatedTextElement: ' + sId + ' not found.' );
                }
            }
            else
            {
                log( 'AnimatedTextElement: no bullet placeholder found' );
            }
        }
        else
        {
            log( 'AnimatedTextElement: no bullet char group found' );
        }
    }

    // In case there are embedded bitmaps we need to clone them
    var aBitmapElemSet = [];
    var aBitmapCloneSet = [];
    var aBitmapPlaceholderSet = getElementsByClassName( aElement, 'BitmapPlaceholder' );
    var i;
    if( aBitmapPlaceholderSet )
    {
        for( i = 0; i < aBitmapPlaceholderSet.length; ++i )
        {
            sId = aBitmapPlaceholderSet[i].getAttribute( 'id' );
            var sBitmapChecksum = sId.substring( 'bitmap-placeholder'.length + 1, sId.length - 1 );
            sId = 'embedded-bitmap(' + sBitmapChecksum + ')';
            aBitmapElemSet[i] = theDocument.getElementById( sId );
            if( aBitmapElemSet[i] )
            {
                aBitmapCloneSet[i] = aBitmapElemSet[i].cloneNode( true );
            }
            else
            {
                log( 'AnimatedTextElement: ' + sId + ' not found.' );
            }
        }
    }


    // Change clone element id.
    this.sParagraphId = sId = aParagraphElement.getAttribute( 'id' );
    aParagraphElement.removeAttribute( 'id' );
    aAnimatableElement.setAttribute( 'id', sId +'.a' );
    if( aBulletCharClone )
        aBulletCharClone.removeAttribute( 'id' );
    for( i = 0; i < aBitmapCloneSet.length; ++i )
    {
        if( aBitmapCloneSet[i] )
            aBitmapCloneSet[i].removeAttribute( 'id' );
    }

    // Set up visibility
    var sVisibilityAttr = aElement.getAttribute( 'visibility' );
    if( !sVisibilityAttr )
        sVisibilityAttr = 'inherit';
    aAnimatableElement.setAttribute( 'visibility', sVisibilityAttr );
    aParagraphElement.setAttribute( 'visibility', 'inherit' );
    this.aGraphicGroupElement.setAttribute( 'visibility', 'inherit' );
    if( aBulletCharElem )
        aBulletCharElem.setAttribute( 'visibility', 'hidden' );
    for( i = 0; i < aBitmapCloneSet.length; ++i )
    {
        if( aBitmapElemSet[i] )
            aBitmapElemSet[i].setAttribute( 'visibility', 'hidden' );
    }

    // Append each element to its parent.
    // <g class='AnimatedElements'>
    //   <g>
    //     <text>
    //       <tspan class='TextParagraph'> ... </tspan>
    //     </text>
    //     <g class='GraphicGroup'>
    //       [<g class='BulletChar'>...</g>]
    //       [<g class='EmbeddedBitmap'>...</g>]
    //       .
    //       .
    //       [<g class='EmbeddedBitmap'>...</g>]
    //     </g>
    //   </g>
    // </g>

    aTextElement.appendChild( aParagraphElement );
    aAnimatableElement.appendChild( aTextElement );

    if( aBulletCharClone )
        this.aGraphicGroupElement.appendChild( aBulletCharClone );
    for( i = 0; i < aBitmapCloneSet.length; ++i )
    {
        if( aBitmapCloneSet[i] )
            this.aGraphicGroupElement.appendChild( aBitmapCloneSet[i] );
    }
    aAnimatableElement.appendChild( this.aGraphicGroupElement );
    aAnimatedElementGroup.appendChild( aAnimatableElement );

    this.aParentTextElement = aElement.parentNode;
    this.aParagraphElement = aElement;
    this.aAnimatedElementGroup = aAnimatedElementGroup;
    this.nRunningAnimations = 0;

    // we collect all hyperlink ids
    this.aHyperlinkIdSet = [];
    var aHyperlinkElementSet = getElementsByClassName( this.aParagraphElement, 'UrlField' );
    var sHyperlinkId;
    for( i = 0; i < aHyperlinkElementSet.length; ++i )
    {
        sHyperlinkId = aHyperlinkElementSet[i].getAttribute( 'id' );
        if( sHyperlinkId )
           this.aHyperlinkIdSet.push( sHyperlinkId );
        else
            log( 'error: AnimatedTextElement constructor: hyperlink element has no id' );
    }

    AnimatedTextElement.superclass.constructor.call( this, aAnimatableElement, aEventMultiplexer );

}
extend( AnimatedTextElement, AnimatedElement );


AnimatedTextElement.prototype.setToElement = function( aElement )
{
    var bRet = AnimatedTextElement.superclass.setToElement.call( this, aElement );
    if( bRet )
    {
        this.aGraphicGroupElement = getElementByClassName( this.aActiveElement, 'GraphicGroup' );
    }
    return ( bRet && this.aGraphicGroupElement );
};

AnimatedTextElement.prototype.notifySlideStart = function( aSlideShowContext )
{
    DBGLOG( 'AnimatedTextElement.notifySlideStart' );
    AnimatedTextElement.superclass.notifySlideStart.call( this, aSlideShowContext );
    this.aGraphicGroupElement = getElementByClassName( this.aActiveElement, 'GraphicGroup' );
    this.restoreBaseTextParagraph();
};

AnimatedTextElement.prototype.notifySlideEnd = function()
{
    DBGLOG( 'AnimatedTextElement.notifySlideEnd' );
    this.aGraphicGroupElement.setAttribute( 'visibility', 'inherit' );
};

AnimatedTextElement.prototype.restoreBaseTextParagraph = function()
{
    var aActiveParagraphElement = this.aActiveElement.firstElementChild.firstElementChild;
    if( aActiveParagraphElement )
    {
        var sVisibilityAttr = this.aActiveElement.getAttribute( 'visibility' );
        if( !sVisibilityAttr || ( sVisibilityAttr === 'visible' ) )
            sVisibilityAttr = 'inherit';
        if( sVisibilityAttr === 'inherit' )
            this.aGraphicGroupElement.setAttribute( 'visibility', 'visible' );
        else
            this.aGraphicGroupElement.setAttribute( 'visibility', 'hidden' );

        var aParagraphClone = aActiveParagraphElement.cloneNode( true );
        aParagraphClone.setAttribute( 'id', this.sParagraphId );
        aParagraphClone.setAttribute( 'visibility', sVisibilityAttr );
        this.aParentTextElement.replaceChild( aParagraphClone, this.aParagraphElement );
        this.aParagraphElement = aParagraphClone;


        var aEventMultiplexer = this.aSlideShowContext.aEventMultiplexer;
        var aHyperlinkIdSet = this.aHyperlinkIdSet;
        var aHyperlinkElementSet = getElementsByClassName( this.aParagraphElement, 'UrlField' );
        var i = 0;
        for( ; i < aHyperlinkIdSet.length; ++i )
        {
            aEventMultiplexer.notifyElementChangedEvent( aHyperlinkIdSet[i], aHyperlinkElementSet[i] );
        }
    }
    this.aActiveElement.setAttribute( 'visibility', 'hidden' );
};

AnimatedTextElement.prototype.notifyAnimationStart = function()
{
    DBGLOG( 'AnimatedTextElement.notifyAnimationStart' );
    if( this.nRunningAnimations === 0 )
    {
        var sVisibilityAttr = this.aParagraphElement.getAttribute( 'visibility' );
        if( !sVisibilityAttr )
            sVisibilityAttr = 'inherit';
        this.aActiveElement.setAttribute( 'visibility', sVisibilityAttr );
        this.aGraphicGroupElement.setAttribute( 'visibility', 'inherit' );
        this.aParagraphElement.setAttribute( 'visibility', 'hidden' );
    }
    ++this.nRunningAnimations;
};

AnimatedTextElement.prototype.notifyAnimationEnd = function()
{
    DBGLOG( 'AnimatedTextElement.notifyAnimationEnd' );
    --this.nRunningAnimations;
    if( this.nRunningAnimations === 0 )
    {
        this.restoreBaseTextParagraph();
    }
};

AnimatedTextElement.prototype.saveState = function( nAnimationNodeId )
{
    if( this.nRunningAnimations === 0 )
    {
        var sVisibilityAttr = this.aParagraphElement.getAttribute( 'visibility' );
        this.aActiveElement.setAttribute( 'visibility', sVisibilityAttr );
        this.aGraphicGroupElement.setAttribute( 'visibility', 'inherit' );
    }
    AnimatedTextElement.superclass.saveState.call( this, nAnimationNodeId );
};

AnimatedTextElement.prototype.restoreState = function( nAnimationNodeId )
{
    var bRet = AnimatedTextElement.superclass.restoreState.call( this, nAnimationNodeId );
    if( bRet )
        this.restoreBaseTextParagraph();
    return bRet;
};




/** Class SlideTransition
 *  This class is responsible for initializing the properties of a slide
 *  transition and create the object that actually will perform the transition.
 *
 *  @param aAnimationsRootElement
 *      The <defs> element wrapping all animations for the related slide.
 *  @param aSlideId
 *      A string representing a slide id.
 */
function SlideTransition( aAnimationsRootElement, aSlideId )
{
    this.sSlideId = aSlideId;
    this.bIsValid = false;
    this.eTransitionType = undefined;
    this.eTransitionSubType = undefined;
    this.bReverseDirection = false;
    this.eTransitionMode = TRANSITION_MODE_IN;
    this.sFadeColor = null;
    this.aDuration = null;
    this.nMinFrameCount = undefined;

    if( aAnimationsRootElement )
    {
        if( aAnimationsRootElement.firstElementChild &&
            ( aAnimationsRootElement.firstElementChild.getAttributeNS( NSS['smil'], 'begin' ) === (this.sSlideId + '.begin') ) )
        {
            var aTransitionFilterElement = aAnimationsRootElement.firstElementChild.firstElementChild;
            if( aTransitionFilterElement && ( aTransitionFilterElement.localName === 'transitionFilter' ) )
            {
                this.aElement = aTransitionFilterElement;
                this.parseElement();
            }
            aAnimationsRootElement.removeChild( aAnimationsRootElement.firstElementChild );
        }
    }
}

SlideTransition.prototype.createSlideTransition = function( aLeavingSlide, aEnteringSlide )
{
    if( !this.isValid() )
        return null;
    if( this.eTransitionType == 0 )
        return null;

    if( !aEnteringSlide )
    {
        log( 'SlideTransition.createSlideTransition: invalid entering slide.' );
        return null;
    }

    var aTransitionInfo = aTransitionInfoTable[this.eTransitionType][this.eTransitionSubType];
    var eTransitionClass = aTransitionInfo['class'];

    switch( eTransitionClass )
    {
        default:
        case TRANSITION_INVALID:
            log( 'SlideTransition.createSlideTransition: transition class: TRANSITION_INVALID' );
            return null;

        case TRANSITION_CLIP_POLYPOLYGON:
            var aParametricPolyPolygon
                    = createClipPolyPolygon( this.eTransitionType, this.eTransitionSubType );
            return new ClippedSlideChange( aLeavingSlide, aEnteringSlide, aParametricPolyPolygon,
                                           aTransitionInfo, this.isDirectionForward() );

        case TRANSITION_SPECIAL:
            switch( this.eTransitionType )
            {
                default:
                    log( 'SlideTransition.createSlideTransition: ' +
                         'transition class: TRANSITION_SPECIAL, ' +
                         'unknown transition type: ' + this.eTransitionType );
                    return null;

                case PUSHWIPE_TRANSITION:
                {
                    var aDirection = null;
                    switch( this.eTransitionSubType )
                    {
                        default:
                            log( 'SlideTransition.createSlideTransition: ' +
                                 'transition type: PUSHWIPE_TRANSITION, ' +
                                 'unknown transition subtype: ' + this.eTransitionSubType );
                            return null;
                        case FROMTOP_TRANS_SUBTYPE:
                            aDirection = { x: 0.0, y: 1.0 };
                            break;
                        case FROMBOTTOM_TRANS_SUBTYPE:
                            aDirection = { x: 0.0, y: -1.0 };
                            break;
                        case FROMLEFT_TRANS_SUBTYPE:
                            aDirection = { x: 1.0, y: 0.0 };
                            break;
                        case FROMRIGHT_TRANS_SUBTYPE:
                            aDirection = { x: -1.0, y: 0.0 };
                            break;
                    }
                    return new MovingSlideChange( aLeavingSlide, aEnteringSlide, aDirection, aDirection );
                }

                case SLIDEWIPE_TRANSITION:
                {
                    var aInDirection = null;
                    switch( this.eTransitionSubType )
                    {
                        default:
                            log( 'SlideTransition.createSlideTransition: ' +
                                 'transition type: SLIDEWIPE_TRANSITION, ' +
                                 'unknown transition subtype: ' + this.eTransitionSubType );
                            return null;
                        case FROMTOP_TRANS_SUBTYPE:
                            aInDirection = { x: 0.0, y: 1.0 };
                            break;
                        case FROMBOTTOM_TRANS_SUBTYPE:
                            aInDirection = { x: 0.0, y: -1.0 };
                            break;
                        case FROMLEFT_TRANS_SUBTYPE:
                            aInDirection = { x: 1.0, y: 0.0 };
                            break;
                        case FROMRIGHT_TRANS_SUBTYPE:
                            aInDirection = { x: -1.0, y: 0.0 };
                            break;
                    }
                    var aNoDirection = { x: 0.0, y: 0.0 };
                    if( !this.bReverseDirection )
                    {
                        return new MovingSlideChange( aLeavingSlide, aEnteringSlide, aNoDirection, aInDirection );
                    }
                    else
                    {
                        return new MovingSlideChange( aLeavingSlide, aEnteringSlide, aInDirection, aNoDirection );
                    }
                }

                case FADE_TRANSITION:
                    switch( this.eTransitionSubType )
                    {
                        default:
                            log( 'SlideTransition.createSlideTransition: ' +
                                 'transition type: FADE_TRANSITION, ' +
                                 'unknown transition subtype: ' + this.eTransitionSubType );
                            return null;
                        case CROSSFADE_TRANS_SUBTYPE:
                            return new FadingSlideChange( aLeavingSlide, aEnteringSlide );
                        case FADEOVERCOLOR_TRANS_SUBTYPE:
                            return new FadingOverColorSlideChange( aLeavingSlide, aEnteringSlide, this.getFadeColor() );
                    }
            }
    }
};

SlideTransition.prototype.parseElement = function()
{
    this.bIsValid = true;
    var aAnimElem = this.aElement;

    // type attribute
    this.eTransitionType = undefined;
    var sTypeAttr = aAnimElem.getAttributeNS( NSS['smil'], 'type' );
    if( sTypeAttr && aTransitionTypeInMap[ sTypeAttr ] )
    {
        this.eTransitionType = aTransitionTypeInMap[ sTypeAttr ];
    }
    else
    {
        this.bIsValid = false;
        log( 'SlideTransition.parseElement: transition type not valid: ' + sTypeAttr );
    }

    // subtype attribute
    this.eTransitionSubType = undefined;
    var sSubTypeAttr = aAnimElem.getAttributeNS( NSS['smil'], 'subtype' );
    if( sSubTypeAttr === null )
        sSubTypeAttr = 'default';
    if( sSubTypeAttr && ( aTransitionSubtypeInMap[ sSubTypeAttr ] !== undefined ) )
    {
        this.eTransitionSubType = aTransitionSubtypeInMap[ sSubTypeAttr ];
    }
    else
    {
        this.bIsValid = false;
        log( 'SlideTransition.parseElement: transition subtype not valid: ' + sSubTypeAttr );
    }

    if( this.bIsValid && aTransitionInfoTable[this.eTransitionType][this.eTransitionSubType] === undefined )
    {
        this.bIsValid = false;
        log( 'SlideTransition.parseElement: transition not valid: type: ' + sTypeAttr + ' subtype: ' + sSubTypeAttr );
    }

    // direction attribute
    this.bReverseDirection = false;
    var sDirectionAttr = aAnimElem.getAttributeNS( NSS['smil'], 'direction' );
    if( sDirectionAttr == 'reverse' )
        this.bReverseDirection = true;

    // fade color
    this.sFadeColor = null;
    if( this.eTransitionType == FADE_TRANSITION &&
        ( this.eTransitionSubType == FADEFROMCOLOR_TRANS_SUBTYPE ||
          this.eTransitionSubType == FADEOVERCOLOR_TRANS_SUBTYPE ||
          this.eTransitionSubType == FADETOCOLOR_TRANS_SUBTYPE ) )
    {
        var sColorAttr = aAnimElem.getAttributeNS( NSS['smil'], 'fadeColor' );
        if( sColorAttr )
            this.sFadeColor = sColorAttr;
        else
            this.sFadeColor='#000000';
    }


    // dur attribute
    this.aDuration = null;
    var sDurAttr = aAnimElem.getAttributeNS( NSS['smil'], 'dur' );
    this.aDuration = new Duration( sDurAttr );
    if( !this.aDuration.isSet() )
    {
        this.aDuration = new Duration( null ); // duration == 0.0
    }

    // set up min frame count value;
    this.nMinFrameCount = ( this.getDuration().isValue() )
        ? ( this.getDuration().getValue() * MINIMUM_FRAMES_PER_SECONDS )
        : MINIMUM_FRAMES_PER_SECONDS;
    if( this.nMinFrameCount < 1.0 )
        this.nMinFrameCount = 1;
    else if( this.nMinFrameCount > MINIMUM_FRAMES_PER_SECONDS )
        this.nMinFrameCount = MINIMUM_FRAMES_PER_SECONDS;

};

SlideTransition.prototype.isValid = function()
{
    return this.bIsValid;
};

SlideTransition.prototype.getTransitionType = function()
{
    return this.eTransitionType;
};

SlideTransition.prototype.getTransitionSubType = function()
{
    return this.eTransitionSubType;
};

SlideTransition.prototype.getTransitionMode = function()
{
    return this.eTransitionMode;
};

SlideTransition.prototype.getFadeColor = function()
{
    return this.sFadeColor;
};

SlideTransition.prototype.isDirectionForward = function()
{
    return !this.bReverseDirection;
};

SlideTransition.prototype.getDuration = function()
{
    return this.aDuration;
};

SlideTransition.prototype.getMinFrameCount = function()
{
    return this.nMinFrameCount;
};

SlideTransition.prototype.info = function()
{

    var sInfo ='slide transition <' + this.sSlideId + '>: ';
    // transition type
    sInfo += ';  type: ' + getKeyByValue(aTransitionTypeInMap, this.getTransitionType());

    // transition subtype
    sInfo += ';  subtype: ' + getKeyByValue(aTransitionSubtypeInMap, this.getTransitionSubType());

    // transition direction
    if( !this.isDirectionForward() )
        sInfo += ';  direction: reverse';

    // transition mode
    sInfo += '; mode: ' + aTransitionModeOutMap[ this.getTransitionMode() ];

    // duration
    if( this.getDuration() )
        sInfo += '; duration: ' + this.getDuration().info();

    return sInfo;
};




// SlideAnimations

function SlideAnimations( aSlideShowContext )
{
    this.aContext = new NodeContext( aSlideShowContext );
    this.aAnimationNodeMap = {};
    this.aAnimatedElementMap = {};
    this.aSourceEventElementMap = {};
    this.aNextEffectEventArray = new NextEffectEventArray();
    this.aInteractiveAnimationSequenceMap = {};
    this.aEventMultiplexer = new EventMultiplexer( aSlideShowContext.aTimerEventQueue );
    this.aRootNode = null;
    this.bElementsParsed = false;

    this.aContext.aAnimationNodeMap = this.aAnimationNodeMap;
    this.aContext.aAnimatedElementMap = this.aAnimatedElementMap;
    this.aContext.aSourceEventElementMap = this.aSourceEventElementMap;

    // We set up a low priority for the invocation of document.handleClick
    // in order to make clicks on shapes, that start interactive animation
    // sequence (on click), have an higher priority.
    this.aEventMultiplexer.registerMouseClickHandler( document, 100 );
}


SlideAnimations.prototype.importAnimations = function( aAnimationRootElement )
{
    if( !aAnimationRootElement )
        return false;

    this.aRootNode = createAnimationTree( aAnimationRootElement, this.aContext );

    return ( this.aRootNode ? true : false );
};

SlideAnimations.prototype.parseElements = function()
{
    if( !this.aRootNode )
        return false;

    // parse all nodes
    if( !this.aRootNode.parseElement() )
        return false;
    else
        this.bElementsParsed = true;
};

SlideAnimations.prototype.elementsParsed = function()
{
    return this.bElementsParsed;
};

SlideAnimations.prototype.isFirstRun = function()
{
    return this.aContext.bFirstRun;
};

SlideAnimations.prototype.isAnimated = function()
{
    if( !this.bElementsParsed )
        return false;

    return this.aRootNode.hasPendingAnimation();
};

SlideAnimations.prototype.start = function()
{
    if( !this.bElementsParsed )
        return false;

    this.chargeSourceEvents();
    this.chargeInterAnimEvents();

    aSlideShow.setSlideEvents( this.aNextEffectEventArray,
                               this.aInteractiveAnimationSequenceMap,
                               this.aEventMultiplexer );

    if( this.aContext.bFirstRun == undefined )
        this.aContext.bFirstRun = true;
    else if( this.aContext.bFirstRun )
        this.aContext.bFirstRun = false;

    // init all nodes
    this.aContext.bIsInvalid = !this.aRootNode.init();
    if( this.aContext.bIsInvalid )
        return false;

    // resolve root node
    return this.aRootNode.resolve();
};

SlideAnimations.prototype.end = function( bLeftEffectsSkipped )
{
    if( !this.bElementsParsed )
        return; // no animations there

    // end root node
    this.aRootNode.deactivate();
    this.aRootNode.end();

    if( bLeftEffectsSkipped && this.isFirstRun() )
    {
        // in case this is the first run and left events have been skipped
        // some next effect events for the slide could not be collected
        // so the next time we should behave as it was the first run again
        this.aContext.bFirstRun = undefined;
    }
    else if( this.isFirstRun() )
    {
        this.aContext.bFirstRun = false;
    }

    this.aContext.bIsInvalid = false;
};

SlideAnimations.prototype.dispose = function()
{
    if( this.aRootNode )
    {
        this.aRootNode.dispose();
    }
};

SlideAnimations.prototype.clearNextEffectEvents = function()
{
    ANIMDBG.print( 'SlideAnimations.clearNextEffectEvents: current slide: ' + nCurSlide );
    this.aNextEffectEventArray.clear();
    this.aContext.bFirstRun = undefined;
};

SlideAnimations.prototype.chargeSourceEvents = function()
{
    for( var id in this.aSourceEventElementMap )
    {
        this.aSourceEventElementMap[id].charge();
    }
};

SlideAnimations.prototype.chargeInterAnimEvents = function()
{
    for( var id in this.aInteractiveAnimationSequenceMap )
    {
        this.aInteractiveAnimationSequenceMap[id].chargeEvents();
    }
};

/**********************************************************************************************
 *      Event classes and helper functions
 **********************************************************************************************/


function Event()
{
    this.nId = Event.getUniqueId();
}


Event.CURR_UNIQUE_ID = 0;

Event.getUniqueId = function()
{
    ++Event.CURR_UNIQUE_ID;
    return Event.CURR_UNIQUE_ID;
};

Event.prototype.getId = function()
{
    return this.nId;
};



function DelayEvent( aFunctor, nTimeout )
{
    DelayEvent.superclass.constructor.call( this );

    this.aFunctor = aFunctor;
    this.nTimeout = nTimeout;
    this.bWasFired = false;
}
extend( DelayEvent, Event );


DelayEvent.prototype.fire = function()
{
    assert( this.isCharged(), 'DelayEvent.fire: assertion isCharged failed' );

    this.bWasFired = true;
    this.aFunctor();
    return true;
};

DelayEvent.prototype.isCharged = function()
{
    return !this.bWasFired;
};

DelayEvent.prototype.getActivationTime = function( nCurrentTime )
{
    return ( this.nTimeout + nCurrentTime );
};

DelayEvent.prototype.dispose = function()
{
    // don't clear unconditionally, because it may currently be executed:
    if( this.isCharged() )
        this.bWasFired = true;
};

DelayEvent.prototype.charge = function()
{
    if( !this.isCharged() )
        this.bWasFired = false;
};



function WakeupEvent( aTimer, aActivityQueue )
{
    WakeupEvent.superclass.constructor.call( this );

    this.aTimer = new ElapsedTime( aTimer );
    this.nNextTime = 0.0;
    this.aActivity = null;
    this.aActivityQueue = aActivityQueue;
}
extend( WakeupEvent, Event );


WakeupEvent.prototype.clone = function()
{
    var aWakeupEvent = new WakeupEvent( this.aTimer.getTimeBase(), this.aActivityQueue );
    aWakeupEvent.nNextTime = this.nNextTime;
    aWakeupEvent.aActivity = this.aActivity;
    return aWakeupEvent;
};

WakeupEvent.prototype.dispose = function()
{
    this.aActivity = null;
};

WakeupEvent.prototype.fire = function()
{
    if( !this.aActivity )
        return false;

    return this.aActivityQueue.addActivity( this.aActivity );
};

WakeupEvent.prototype.isCharged = function()
{
    // this event won't expire, we fire every time we're
    // re-inserted into the event queue.
    return true;
};

WakeupEvent.prototype.getActivationTime = function( nCurrentTime )
{
    var nElapsedTime = this.aTimer.getElapsedTime();

    return Math.max( nCurrentTime, nCurrentTime - nElapsedTime + this.nNextTime );
};

WakeupEvent.prototype.start = function()
{
    this.aTimer.reset();
};

WakeupEvent.prototype.setNextTimeout = function( nNextTime )
{
    this.nNextTime = nNextTime;
};

WakeupEvent.prototype.setActivity = function( aActivity )
{
    this.aActivity = aActivity;
};



function makeEvent( aFunctor )
{
    return new DelayEvent( aFunctor, 0.0 );
}




function makeDelay( aFunctor, nTimeout )
{
    return new DelayEvent( aFunctor, nTimeout );
}




function registerEvent( nNodeId, aTiming, aEvent, aNodeContext )
{
    var aSlideShowContext = aNodeContext.aContext;
    var eTimingType = aTiming.getType();

    registerEvent.DBG( aTiming );

    if( eTimingType == OFFSET_TIMING )
    {
        aSlideShowContext.aTimerEventQueue.addEvent( aEvent );
    }
    else if ( aNodeContext.bFirstRun )
    {
        var aEventMultiplexer = aSlideShowContext.aEventMultiplexer;
        if( !aEventMultiplexer )
        {
            log( 'registerEvent: event multiplexer not initialized' );
            return;
        }
        var aNextEffectEventArray = aSlideShowContext.aNextEffectEventArray;
        if( !aNextEffectEventArray )
        {
            log( 'registerEvent: next effect event array not initialized' );
            return;
        }
        var aInteractiveAnimationSequenceMap =
            aSlideShowContext.aInteractiveAnimationSequenceMap;
        if( !aInteractiveAnimationSequenceMap )
        {
            log( 'registerEvent: interactive animation sequence map not initialized' );
            return;
        }

        switch( eTimingType )
        {
            case EVENT_TIMING:
                var eEventType = aTiming.getEventType();
                var sEventBaseElemId = aTiming.getEventBaseElementId();
                if( sEventBaseElemId )
                {
                    var aEventBaseElem = document.getElementById( sEventBaseElemId );
                    if( !aEventBaseElem )
                    {
                        log( 'generateEvent: EVENT_TIMING: event base element not found: ' + sEventBaseElemId );
                        return;
                    }
                    var aSourceEventElement = aNodeContext.makeSourceEventElement( sEventBaseElemId, aEventBaseElem );

                    if( !aInteractiveAnimationSequenceMap[ nNodeId ] )
                    {
                        aInteractiveAnimationSequenceMap[ nNodeId ] = new InteractiveAnimationSequence(nNodeId);
                    }

                    var bEventRegistered = false;
                    switch( eEventType )
                    {
                        case EVENT_TRIGGER_ON_CLICK:
                            aEventMultiplexer.registerEvent( eEventType, aSourceEventElement.getId(), aEvent );
                            aEventMultiplexer.registerRewindedEffectHandler( aSourceEventElement.getId(),
                                                                             bind2( aSourceEventElement.charge, aSourceEventElement ) );
                            bEventRegistered = true;
                            break;
                        default:
                            log( 'generateEvent: not handled event type: ' + eEventType );
                    }
                    if( bEventRegistered )
                    {
                        var aStartEvent = aInteractiveAnimationSequenceMap[ nNodeId ].getStartEvent();
                        var aEndEvent = aInteractiveAnimationSequenceMap[ nNodeId ].getEndEvent();
                        aEventMultiplexer.registerEvent( eEventType, aSourceEventElement.getId(), aStartEvent );
                        aEventMultiplexer.registerEvent( EVENT_TRIGGER_END_EVENT, nNodeId, aEndEvent );
                        aEventMultiplexer.registerRewindedEffectHandler(
                            nNodeId,
                            bind2( InteractiveAnimationSequence.prototype.chargeEvents,
                                   aInteractiveAnimationSequenceMap[ nNodeId ] )
                        );
                    }
                }
                else  // no base event element present
                {
                    switch( eEventType )
                    {
                        case EVENT_TRIGGER_ON_NEXT_EFFECT:
                            aNextEffectEventArray.appendEvent( aEvent );
                            break;
                        default:
                            log( 'generateEvent: not handled event type: ' + eEventType );
                    }
                }
                break;
            case SYNCBASE_TIMING:
                eEventType = aTiming.getEventType();
                sEventBaseElemId = aTiming.getEventBaseElementId();
                if( sEventBaseElemId )
                {
                    var aAnimationNode = aNodeContext.aAnimationNodeMap[ sEventBaseElemId ];
                    if( !aAnimationNode )
                    {
                        log( 'generateEvent: SYNCBASE_TIMING: event base element not found: ' + sEventBaseElemId );
                        return;
                    }
                    aEventMultiplexer.registerEvent( eEventType, aAnimationNode.getId(), aEvent );
                }
                else
                {
                    log( 'generateEvent: SYNCBASE_TIMING: event base element not specified' );
                }
                break;
            default:
                log( 'generateEvent: not handled timing type: ' + eTimingType );
        }
    }
}

registerEvent.DEBUG = aRegisterEventDebugPrinter.isEnabled();

registerEvent.DBG = function( aTiming, nTime )
{
    if( registerEvent.DEBUG )
    {
        aRegisterEventDebugPrinter.print( 'registerEvent( timing: ' + aTiming.info() + ' )', nTime );
    }
};




function SourceEventElement( sId, aElement, aEventMultiplexer )
{
    this.sId = sId;
    this.aElement = aElement;
    this.aEventMultiplexer = aEventMultiplexer;

    this.aEventMultiplexer.registerMouseClickHandler( this, 1000 );

    this.bClickHandled = false;
    this.bIsPointerOver = false;
    this.aElement.addEventListener( 'mouseover', bind2( SourceEventElement.prototype.onMouseEnter, this), false );
    this.aElement.addEventListener( 'mouseout', bind2( SourceEventElement.prototype.onMouseLeave, this), false );
}

SourceEventElement.prototype.getId = function()
{
    return this.sId;
};

SourceEventElement.prototype.onMouseEnter = function()
{
    this.bIsPointerOver = true;
    this.setPointerCursor();
};

SourceEventElement.prototype.onMouseLeave = function()
{
    this.bIsPointerOver = false;
    this.setDefaultCursor();
};

SourceEventElement.prototype.charge = function()
{
    this.bClickHandled = false;
    this.setPointerCursor();
};

SourceEventElement.prototype.handleClick = function( /*aMouseEvent*/ )
{
    if( !this.bIsPointerOver ) return false;

    if( this.bClickHandled )
        return false;

    this.aEventMultiplexer.notifyEvent( EVENT_TRIGGER_ON_CLICK, this.getId() );
    aSlideShow.update();
    this.bClickHandled = true;
    this.setDefaultCursor();
    return true;
};

SourceEventElement.prototype.setPointerCursor = function()
{
    if( this.bClickHandled )
        return;

    this.aElement.setAttribute( 'style', 'cursor: pointer' );
};

SourceEventElement.prototype.setDefaultCursor = function()
{
    this.aElement.setAttribute( 'style', 'cursor: default' );
};



function HyperlinkElement( sId, aEventMultiplexer )
{
    var aElement = document.getElementById( sId );
    if( !aElement )
    {
        log( 'error: HyperlinkElement: no element with id: <' + sId + '> found' );
        return;
    }
    if( !aEventMultiplexer )
    {
        log( 'AnimatedElement constructor: event multiplexer is not valid' );
    }

    this.sId = sId;
    this.aElement = aElement;
    this.aEventMultiplexer = aEventMultiplexer;
    this.nTargetSlideIndex = undefined;

    this.sURL = getNSAttribute( 'xlink', this.aElement, 'href' );
    if( this.sURL )
    {
        if( this.sURL[0] === '#' )
        {
            if( this.sURL.substr(1, 5) === 'Slide' )
            {
                var sSlideIndex = this.sURL.split( ' ' )[1];
                this.nTargetSlideIndex = parseInt( sSlideIndex ) - 1;
            }
        }

        this.aEventMultiplexer.registerElementChangedHandler( this.sId, bind2( HyperlinkElement.prototype.onElementChanged, this) );
        this.aEventMultiplexer.registerMouseClickHandler( this, 1100 );

        this.bIsPointerOver = false;
        this.mouseEnterHandler = bind2( HyperlinkElement.prototype.onMouseEnter, this);
        this.mouseLeaveHandler = bind2( HyperlinkElement.prototype.onMouseLeave, this);
        this.aElement.addEventListener( 'mouseover', this.mouseEnterHandler, false );
        this.aElement.addEventListener( 'mouseout', this.mouseLeaveHandler, false );
    }
    else
    {
        log( 'warning: HyperlinkElement(' + this.sId + '): url is empty' );
    }
}

HyperlinkElement.prototype.onElementChanged = function( aElement )
{
    if( !aElement )
    {
        log( 'error: HyperlinkElement: passed element is not valid' );
        return;
    }

    if( this.sURL )
    {
        this.aElement.removeEventListener( 'mouseover', this.mouseEnterHandler, false );
        this.aElement.removeEventListener( 'mouseout', this.mouseLeaveHandler, false );
        this.aElement = aElement;
        this.aElement.addEventListener( 'mouseover', this.mouseEnterHandler, false );
        this.aElement.addEventListener( 'mouseout', this.mouseLeaveHandler, false );
    }
};

HyperlinkElement.prototype.onMouseEnter = function()
{
    this.bIsPointerOver = true;
    this.setPointerCursor();
};

HyperlinkElement.prototype.onMouseLeave = function()
{
    this.bIsPointerOver = false;
    this.setDefaultCursor();
};

HyperlinkElement.prototype.handleClick = function( )
{
    if( !this.bIsPointerOver ) return false;

    if( this.nTargetSlideIndex !== undefined )
    {
        aSlideShow.displaySlide( this.nTargetSlideIndex, true );
    }
    else
    {
        var aWindowObject = document.defaultView;
        if( aWindowObject )
        {
            aWindowObject.open( this.sURL, this.sId );
        }
        else
        {
            log( 'error: HyperlinkElement.handleClick: invalid window object.' );
        }
    }

    return true;
};

HyperlinkElement.prototype.setPointerCursor = function()
{
    if( this.bClickHandled )
        return;

    this.aElement.setAttribute( 'style', 'cursor: pointer' );
};

HyperlinkElement.prototype.setDefaultCursor = function()
{
    this.aElement.setAttribute( 'style', 'cursor: default' );
};



function InteractiveAnimationSequence( nId )
{
    this.nId = nId;
    this.bIsRunning = false;
    this.aStartEvent = null;
    this.aEndEvent = null;
}

InteractiveAnimationSequence.prototype.getId = function()
{
    return this.nId;
};

InteractiveAnimationSequence.prototype.getStartEvent = function()
{
    if( !this.aStartEvent )
    {
        this.aStartEvent =
            makeEvent( bind2( InteractiveAnimationSequence.prototype.start, this ) );
    }
    return this.aStartEvent;
};

InteractiveAnimationSequence.prototype.getEndEvent = function()
{
    if( !this.aEndEvent )
    {
        this.aEndEvent =
            makeEvent( bind2( InteractiveAnimationSequence.prototype.end, this ) );
    }
    return this.aEndEvent;
};

InteractiveAnimationSequence.prototype.chargeEvents = function()
{
    if( this.aStartEvent )      this.aStartEvent.charge();
    if( this.aEndEvent )        this.aEndEvent.charge();
};

InteractiveAnimationSequence.prototype.isRunning = function()
{
    return this.bIsRunning;
};

InteractiveAnimationSequence.prototype.start = function()
{
    aSlideShow.notifyInteractiveAnimationSequenceStart( this.getId() );
    this.bIsRunning = true;
};

InteractiveAnimationSequence.prototype.end = function()
{
    aSlideShow.notifyInteractiveAnimationSequenceEnd( this.getId() );
    this.bIsRunning = false;
};


/** class PriorityEntry
 *  It provides an entry type for priority queues.
 *  Higher is the value of nPriority higher is the priority of the created entry.
 *
 *  @param aValue
 *      The object to be prioritized.
 *  @param nPriority
 *      An integral number representing the object priority.
 *
 */
function PriorityEntry( aValue, nPriority )
{
    this.aValue = aValue;
    this.nPriority = nPriority;
}

/** EventEntry.compare
 *  Compare priority of two entries.
 *
 *  @param aLhsEntry
 *      An instance of type PriorityEntry.
 *  @param aRhsEntry
 *      An instance of type PriorityEntry.
 *  @return Integer
 *      -1 if the left entry has lower priority of the right entry,
 *       1 if the left entry has higher priority of the right entry,
 *       0 if the two entry have the same priority
 */
PriorityEntry.compare = function( aLhsEntry, aRhsEntry )
{
    if ( aLhsEntry.nPriority < aRhsEntry.nPriority )
    {
        return -1;
    }
    else if (aLhsEntry.nPriority > aRhsEntry.nPriority)
    {
        return 1;
    }
    else
    {
        return 0;
    }
};




function EventMultiplexer( aTimerEventQueue )
{
    this.nId = EventMultiplexer.getUniqueId();
    this.aTimerEventQueue = aTimerEventQueue;
    this.aEventMap = {};
    this.aAnimationsEndHandler = null;
    this.aSkipEffectEndHandlerSet = [];
    this.aMouseClickHandlerSet = new PriorityQueue( PriorityEntry.compare );
    this.aSkipEffectEvent = null;
    this.aRewindCurrentEffectEvent = null;
    this.aRewindLastEffectEvent = null;
    this.aSkipInteractiveEffectEventSet = {};
    this.aRewindRunningInteractiveEffectEventSet = {};
    this.aRewindEndedInteractiveEffectEventSet = {};
    this.aRewindedEffectHandlerSet = {};
    this.aElementChangedHandlerSet = {};
}

EventMultiplexer.CURR_UNIQUE_ID = 0;

EventMultiplexer.getUniqueId = function()
{
    ++EventMultiplexer.CURR_UNIQUE_ID;
    return EventMultiplexer.CURR_UNIQUE_ID;
};

EventMultiplexer.prototype.getId = function()
{
    return this.nId;
};

EventMultiplexer.prototype.hasRegisteredMouseClickHandlers = function()
{
    return !this.aMouseClickHandlerSet.isEmpty();
};

EventMultiplexer.prototype.registerMouseClickHandler = function( aHandler, nPriority )
{
    var aHandlerEntry = new PriorityEntry( aHandler, nPriority );
    this.aMouseClickHandlerSet.push( aHandlerEntry );
};

EventMultiplexer.prototype.notifyMouseClick = function( aMouseEvent )
{
    var aMouseClickHandlerSet = this.aMouseClickHandlerSet.clone();
    while( !aMouseClickHandlerSet.isEmpty() )
    {
        var aHandlerEntry = aMouseClickHandlerSet.top();
        aMouseClickHandlerSet.pop();
        if( aHandlerEntry.aValue.handleClick( aMouseEvent ) )
            break;
    }
};

EventMultiplexer.prototype.registerEvent = function( eEventType, aNotifierId, aEvent )
{
    this.DBG( 'registerEvent', eEventType, aNotifierId );
    if( !this.aEventMap[ eEventType ] )
    {
        this.aEventMap[ eEventType ] = {};
    }
    if( !this.aEventMap[ eEventType ][ aNotifierId ] )
    {
        this.aEventMap[ eEventType ][ aNotifierId ] = [];
    }
    this.aEventMap[ eEventType ][ aNotifierId ].push( aEvent );
};


EventMultiplexer.prototype.notifyEvent = function( eEventType, aNotifierId )
{
    this.DBG( 'notifyEvent', eEventType, aNotifierId );
    if( this.aEventMap[ eEventType ] )
    {
        if( this.aEventMap[ eEventType ][ aNotifierId ] )
        {
            var aEventArray = this.aEventMap[ eEventType ][ aNotifierId ];
            var nSize = aEventArray.length;
            for( var i = 0; i < nSize; ++i )
            {
                this.aTimerEventQueue.addEvent( aEventArray[i] );
            }
        }
    }
};

EventMultiplexer.prototype.registerAnimationsEndHandler = function( aHandler )
{
    this.aAnimationsEndHandler = aHandler;
};

EventMultiplexer.prototype.notifyAnimationsEndEvent = function()
{
    if( this.aAnimationsEndHandler )
        this.aAnimationsEndHandler();
};

EventMultiplexer.prototype.registerNextEffectEndHandler = function( aHandler )
{
    this.aSkipEffectEndHandlerSet.push( aHandler );
};

EventMultiplexer.prototype.notifyNextEffectEndEvent = function()
{
    var nSize = this.aSkipEffectEndHandlerSet.length;
    for( var i = 0; i < nSize; ++i )
    {
        (this.aSkipEffectEndHandlerSet[i])();
    }
    this.aSkipEffectEndHandlerSet = [];
};

EventMultiplexer.prototype.registerSkipEffectEvent = function( aEvent )
{
    this.aSkipEffectEvent = aEvent;
};

EventMultiplexer.prototype.notifySkipEffectEvent = function()
{
    if( this.aSkipEffectEvent )
    {
        this.aTimerEventQueue.addEvent( this.aSkipEffectEvent );
        this.aSkipEffectEvent = null;
    }
};

EventMultiplexer.prototype.registerRewindCurrentEffectEvent = function( aEvent )
{
    this.aRewindCurrentEffectEvent = aEvent;
};

EventMultiplexer.prototype.notifyRewindCurrentEffectEvent = function()
{
    if( this.aRewindCurrentEffectEvent )
    {
        this.aTimerEventQueue.addEvent( this.aRewindCurrentEffectEvent );
        this.aRewindCurrentEffectEvent = null;
    }
};

EventMultiplexer.prototype.registerRewindLastEffectEvent = function( aEvent )
{
    this.aRewindLastEffectEvent = aEvent;
};

EventMultiplexer.prototype.notifyRewindLastEffectEvent = function()
{
    if( this.aRewindLastEffectEvent )
    {
        this.aTimerEventQueue.addEvent( this.aRewindLastEffectEvent );
        this.aRewindLastEffectEvent = null;
    }
};

EventMultiplexer.prototype.registerSkipInteractiveEffectEvent = function( nNotifierId, aEvent )
{
    this.aSkipInteractiveEffectEventSet[ nNotifierId ] = aEvent;
};

EventMultiplexer.prototype.notifySkipInteractiveEffectEvent = function( nNotifierId )
{
    if( this.aSkipInteractiveEffectEventSet[ nNotifierId ] )
    {
        this.aTimerEventQueue.addEvent( this.aSkipInteractiveEffectEventSet[ nNotifierId ] );
    }
};

EventMultiplexer.prototype.registerRewindRunningInteractiveEffectEvent = function( nNotifierId, aEvent )
{
    this.aRewindRunningInteractiveEffectEventSet[ nNotifierId ] = aEvent;
};

EventMultiplexer.prototype.notifyRewindRunningInteractiveEffectEvent = function( nNotifierId )
{
    if( this.aRewindRunningInteractiveEffectEventSet[ nNotifierId ] )
    {
        this.aTimerEventQueue.addEvent( this.aRewindRunningInteractiveEffectEventSet[ nNotifierId ] );
    }
};

EventMultiplexer.prototype.registerRewindEndedInteractiveEffectEvent = function( nNotifierId, aEvent )
{
    this.aRewindEndedInteractiveEffectEventSet[ nNotifierId ] = aEvent;
};

EventMultiplexer.prototype.notifyRewindEndedInteractiveEffectEvent = function( nNotifierId )
{
    if( this.aRewindEndedInteractiveEffectEventSet[ nNotifierId ] )
    {
        this.aTimerEventQueue.addEvent( this.aRewindEndedInteractiveEffectEventSet[ nNotifierId ] );
    }
};

EventMultiplexer.prototype.registerRewindedEffectHandler = function( aNotifierId, aHandler )
{
    this.aRewindedEffectHandlerSet[ aNotifierId ] = aHandler;
};

EventMultiplexer.prototype.notifyRewindedEffectEvent = function( aNotifierId )
{
    if( this.aRewindedEffectHandlerSet[ aNotifierId ] )
    {
        (this.aRewindedEffectHandlerSet[ aNotifierId ])();
    }
};

EventMultiplexer.prototype.registerElementChangedHandler = function( aNotifierId, aHandler )
{
    this.aElementChangedHandlerSet[ aNotifierId ] = aHandler;
};

EventMultiplexer.prototype.notifyElementChangedEvent = function( aNotifierId, aElement )
{
    if( this.aElementChangedHandlerSet[ aNotifierId ] )
    {
        (this.aElementChangedHandlerSet[ aNotifierId ])( aElement );
    }
};

EventMultiplexer.DEBUG = aEventMultiplexerDebugPrinter.isEnabled();

EventMultiplexer.prototype.DBG = function( sMethodName, eEventType, aNotifierId, nTime )
{
    if( EventMultiplexer.DEBUG )
    {
        var sInfo = 'EventMultiplexer.' + sMethodName;
        sInfo += '( type: ' + aEventTriggerOutMap[ eEventType ];
        sInfo += ', notifier: ' + aNotifierId + ' )';
        aEventMultiplexerDebugPrinter.print( sInfo, nTime );
    }
};



/**********************************************************************************************
 *      Interpolator Handler and KeyStopLerp
 **********************************************************************************************/

var aInterpolatorHandler = {};

aInterpolatorHandler.getInterpolator = function( eCalcMode, eValueType, eValueSubtype )
{
    var bHasSubtype = ( typeof( eValueSubtype ) === typeof( 0 ) );

    if( !bHasSubtype && aInterpolatorHandler.aLerpFunctorMap[ eCalcMode ][ eValueType ] )
    {
        return aInterpolatorHandler.aLerpFunctorMap[ eCalcMode ][ eValueType ];
    }
    else if( bHasSubtype && aInterpolatorHandler.aLerpFunctorMap[ eCalcMode ][ eValueType ][ eValueSubtype ] )
    {
        return aInterpolatorHandler.aLerpFunctorMap[ eCalcMode ][ eValueType ][ eValueSubtype ];
    }
    else
    {
        log( 'aInterpolatorHandler.getInterpolator: not found any valid interpolator for calc mode '
             + aCalcModeOutMap[eCalcMode]  + ' and value type ' + aValueTypeOutMap[eValueType]  );
        return null;
    }
};

aInterpolatorHandler.aLerpFunctorMap = [];
aInterpolatorHandler.aLerpFunctorMap[ CALC_MODE_DISCRETE ] = [];
aInterpolatorHandler.aLerpFunctorMap[ CALC_MODE_LINEAR ] = [];


// interpolators for linear calculation

aInterpolatorHandler.aLerpFunctorMap[ CALC_MODE_LINEAR ][ NUMBER_PROPERTY ] =
    function ( nFrom, nTo, nT )
    {
        return ( ( 1.0 - nT )* nFrom + nT * nTo );
    };

aInterpolatorHandler.aLerpFunctorMap[ CALC_MODE_LINEAR ][ COLOR_PROPERTY ] = [];

aInterpolatorHandler.aLerpFunctorMap[ CALC_MODE_LINEAR ][ COLOR_PROPERTY ][ COLOR_SPACE_RGB ] =
    function ( nFrom, nTo, nT )
    {
        return RGBColor.interpolate( nFrom, nTo, nT );
    };

// For HSLColor we do not return the interpolator but a function
// that generate the interpolator. The AnimationColorNode is 'aware' of that.
aInterpolatorHandler.aLerpFunctorMap[ CALC_MODE_LINEAR ][ COLOR_PROPERTY ][ COLOR_SPACE_HSL ] =
    function ( bCCW  )
    {
        return  function ( nFrom, nTo, nT )
                {
                    return HSLColor.interpolate( nFrom, nTo, nT, bCCW );
                };
    };

aInterpolatorHandler.aLerpFunctorMap[ CALC_MODE_LINEAR ][ TUPLE_NUMBER_PROPERTY ] =
    function ( aFrom, aTo, nT )
    {
        var aRes = [];
        for( var i = 0; i < aFrom.length; ++i )
        {
            aRes.push( ( 1.0 - nT )* aFrom[i] + nT * aTo[i] );
        }
        return aRes;
    };




function KeyStopLerp( aValueList )
{
    KeyStopLerp.validateInput( aValueList );

    this.aKeyStopList = [];
    this.nLastIndex = 0;
    this.nKeyStopDistance = aValueList[1] - aValueList[0];
    if( this.nKeyStopDistance <= 0 )
        this.nKeyStopDistance = 0.001;

    for( var i = 0; i < aValueList.length; ++i )
        this.aKeyStopList.push( aValueList[i] );

    this.nUpperBoundIndex = this.aKeyStopList.length - 2;
}


KeyStopLerp.validateInput = function( aValueList )
{
    var nSize = aValueList.length;
    assert( nSize > 1, 'KeyStopLerp.validateInput: key stop vector must have two entries or more' );

    for( var i = 1; i < nSize; ++i )
    {
        if( aValueList[i-1] > aValueList[i] )
            log( 'KeyStopLerp.validateInput: time vector is not sorted in ascending order!' );
    }
};

KeyStopLerp.prototype.reset = function()
{
    KeyStopLerp.validateInput( this.aKeyStopList );
    this.nLastIndex = 0;
    this.nKeyStopDistance = this.aKeyStopList[1] - this.aKeyStopList[0];
    if( this.nKeyStopDistance <= 0 )
        this.nKeyStopDistance = 0.001;

};

KeyStopLerp.prototype.lerp = function( nAlpha )
{
    if( nAlpha > this.aKeyStopList[ this.nLastIndex + 1 ] )
    {
        do
        {
            var nIndex = this.nLastIndex + 1;
            this.nLastIndex = clamp( nIndex, 0, this.nUpperBoundIndex );
            this.nKeyStopDistance = this.aKeyStopList[ this.nLastIndex + 1 ] - this.aKeyStopList[ this.nLastIndex ];
        }
        while( ( this.nKeyStopDistance <= 0 ) && ( this.nLastIndex < this.nUpperBoundIndex ) );
    }

    var nRawLerp = ( nAlpha - this.aKeyStopList[ this.nLastIndex ] ) / this.nKeyStopDistance;

    nRawLerp = clamp( nRawLerp, 0.0, 1.0 );

    var aResult = {};
    aResult.nIndex = this.nLastIndex;
    aResult.nLerp = nRawLerp;

    return aResult;
};

KeyStopLerp.prototype.lerp_ported = function( nAlpha )
{
    if( ( this.aKeyStopList[ this.nLastIndex ] < nAlpha ) ||
        ( this.aKeyStopList[ this.nLastIndex + 1 ] >= nAlpha ) )
    {
        var i = 0;
        for( ; i < this.aKeyStopList.length; ++i )
        {
            if( this.aKeyStopList[i] >= nAlpha )
                break;
        }
        if( this.aKeyStopList[i] > nAlpha )
            --i;
        var nIndex = i - 1;
        this.nLastIndex = clamp( nIndex, 0, this.aKeyStopList.length - 2 );
    }

    var nRawLerp = ( nAlpha - this.aKeyStopList[ this.nLastIndex ] ) /
                       ( this.aKeyStopList[ this.nLastIndex+1 ] - this.aKeyStopList[ this.nLastIndex ] );

    nRawLerp = clamp( nRawLerp, 0.0, 1.0 );

    var aResult = {};
    aResult.nIndex = this.nLastIndex;
    aResult.nLerp = nRawLerp;

    return aResult;
};



/**********************************************************************************************
 *      Operators
 **********************************************************************************************/

var aOperatorSetMap = [];

// number operators
aOperatorSetMap[ NUMBER_PROPERTY ] = {};

aOperatorSetMap[ NUMBER_PROPERTY ].equal = function( a, b )
{
    return ( a === b );
};

aOperatorSetMap[ NUMBER_PROPERTY ].add = function( a, b )
{
    return ( a + b );
};

aOperatorSetMap[ NUMBER_PROPERTY ].scale = function( k, v )
{
    return ( k * v );
};

// color operators
aOperatorSetMap[ COLOR_PROPERTY ] = {};

aOperatorSetMap[ COLOR_PROPERTY ].equal = function( a, b )
{
    return a.equal( b );
};

aOperatorSetMap[ COLOR_PROPERTY ].add = function( a, b )
{
    var c = a.clone();
    c.add( b );
    return c;
};

aOperatorSetMap[ COLOR_PROPERTY ].scale = function( k, v )
{
    var r = v.clone();
    r.scale( k );
    return r;
};

// enum operators
aOperatorSetMap[ ENUM_PROPERTY ] = {};

aOperatorSetMap[ ENUM_PROPERTY ].equal = function( a, b )
{
    return ( a === b );
};

aOperatorSetMap[ ENUM_PROPERTY ].add = function( a )
{
    return a;
};

aOperatorSetMap[ ENUM_PROPERTY ].scale = function( k, v )
{
    return v;
};

// string operators
aOperatorSetMap[ STRING_PROPERTY ] = aOperatorSetMap[ ENUM_PROPERTY ];

// bool operators
aOperatorSetMap[ BOOL_PROPERTY ] = aOperatorSetMap[ ENUM_PROPERTY ];

// tuple number operators
aOperatorSetMap[ TUPLE_NUMBER_PROPERTY ] = {};

aOperatorSetMap[ TUPLE_NUMBER_PROPERTY ].equal = function( a, b )
{
    assert( a.length === b.length, 'Tuples length mismatch.' );
    return ( a.toString() === b.toString() );
};

aOperatorSetMap[ TUPLE_NUMBER_PROPERTY ].add = function( a, b )
{
    assert( a.length === b.length, 'Tuples length mismatch.' );
    var r = [];
    for( var i = 0; i < a.length; ++i )
    {
        r.push(a[i] + b[i]);
    }
    return r;
};

aOperatorSetMap[ TUPLE_NUMBER_PROPERTY ].scale = function( k, v )
{
    var r = [];
    for( var i = 0; i < v.length; ++i )
    {
        r.push(k * v[i]);
    }
    return r;
};




/**********************************************************************************************
 *      Activity Class Hierarchy
 **********************************************************************************************/


function ActivityParamSet()
{
    this.aEndEvent = null;
    this.aWakeupEvent = null;
    this.aTimerEventQueue = null;
    this.aActivityQueue = null;
    this.nMinDuration = undefined;
    this.nMinNumberOfFrames = MINIMUM_FRAMES_PER_SECONDS;
    this.bAutoReverse = false;
    this.nRepeatCount = 1.0;
    this.nAccelerationFraction = 0.0;
    this.nDecelerationFraction = 0.0;
    this.nSlideWidth = undefined;
    this.nSlideHeight = undefined;
    this.aFormula = null;
    this.aDiscreteTimes = [];
}


function AnimationActivity()
{
    this.nId = AnimationActivity.getUniqueId();
}


AnimationActivity.CURR_UNIQUE_ID = 0;

AnimationActivity.getUniqueId = function()
{
    ++AnimationActivity.CURR_UNIQUE_ID;
    return AnimationActivity.CURR_UNIQUE_ID;
};

AnimationActivity.prototype.getId = function()
{
    return this.nId;
};




function SetActivity( aCommonParamSet, aAnimation, aToAttr  )
{
    SetActivity.superclass.constructor.call( this );

    this.aAnimation = aAnimation;
    this.aTargetElement = null;
    this.aEndEvent = aCommonParamSet.aEndEvent;
    this.aTimerEventQueue = aCommonParamSet.aTimerEventQueue;
    this.aToAttr = aToAttr;
    this.bIsActive = true;
}
extend( SetActivity, AnimationActivity );


SetActivity.prototype.activate = function( aEndEvent )
{
    this.aEndEvent = aEndEvent;
    this.bIsActive = true;
};

SetActivity.prototype.dispose = function()
{
    this.bIsActive = false;
    if( this.aEndEvent && this.aEndEvent.isCharged() )
        this.aEndEvent.dispose();
};

SetActivity.prototype.calcTimeLag = function()
{
    return 0.0;
};

SetActivity.prototype.perform = function()
{
    if( !this.isActive() )
        return false;

    // we're going inactive immediately:
    this.bIsActive = false;

    if( this.aAnimation && this.aTargetElement )
    {
        this.aAnimation.start( this.aTargetElement );
        this.aAnimation.perform( this.aToAttr );
        this.aAnimation.end();
    }

    if( this.aEndEvent )
        this.aTimerEventQueue.addEvent( this.aEndEvent );

};

SetActivity.prototype.isActive = function()
{
    return this.bIsActive;
};

SetActivity.prototype.dequeued = function()
{
    // empty body
};

SetActivity.prototype.end = function()
{
    this.perform();
};

SetActivity.prototype.setTargets = function( aTargetElement )
{
    assert( aTargetElement, 'SetActivity.setTargets: target element is not valid' );
    this.aTargetElement = aTargetElement;
};




function ActivityBase( aCommonParamSet )
{
    ActivityBase.superclass.constructor.call( this );

    this.aTargetElement = null;
    this.aEndEvent = aCommonParamSet.aEndEvent;
    this.aTimerEventQueue = aCommonParamSet.aTimerEventQueue;
    this.nRepeats = aCommonParamSet.nRepeatCount;
    this.nAccelerationFraction = aCommonParamSet.nAccelerationFraction;
    this.nDecelerationFraction = aCommonParamSet.nDecelerationFraction;
    this.bAutoReverse = aCommonParamSet.bAutoReverse;

    this.bFirstPerformCall = true;
    this.bIsActive = true;

}
extend( ActivityBase, AnimationActivity );


ActivityBase.prototype.activate = function( aEndEvent )
{
    this.aEndEvent = aEndEvent;
    this.bFirstPerformCall = true;
    this.bIsActive = true;
};

ActivityBase.prototype.dispose = function()
{
    // deactivate
    this.bIsActive = false;

    // dispose event
    if( this.aEndEvent )
        this.aEndEvent.dispose();

    this.aEndEvent = null;
};

ActivityBase.prototype.perform = function()
{
    // still active?
    if( !this.isActive() )
        return false; // no, early exit.

    assert( !this.bFirstPerformCall, 'ActivityBase.perform: assertion (!this.FirstPerformCall) failed' );

    return true;
};

ActivityBase.prototype.calcTimeLag = function()
{
    // TODO(Q1): implement different init process!
    if( this.isActive() && this.bFirstPerformCall )
    {
        this.bFirstPerformCall = false;

        // notify derived classes that we're
        // starting now
        this.startAnimation();
    }
    return 0.0;
};

ActivityBase.prototype.isActive = function()
{
    return this.bIsActive;
};

ActivityBase.prototype.isDisposed = function()
{
    return ( !this.bIsActive && !this.aEndEvent );
};

ActivityBase.prototype.dequeued = function()
{
    if( !this.isActive() )
        this.endAnimation();
};

ActivityBase.prototype.setTargets = function( aTargetElement )
{
    assert( aTargetElement, 'ActivityBase.setTargets: target element is not valid' );

    this.aTargetElement = aTargetElement;
};

ActivityBase.prototype.startAnimation = function()
{
    throw ( 'ActivityBase.startAnimation: abstract method invoked' );
};

ActivityBase.prototype.endAnimation = function()
{
    throw ( 'ActivityBase.endAnimation: abstract method invoked' );
};

ActivityBase.prototype.endActivity = function()
{
    // this is a regular activity end
    this.bIsActive = false;

    // Activity is ending, queue event, then
    if( this.aEndEvent )
        this.aTimerEventQueue.addEvent( this.aEndEvent );

    this.aEndEvent = null;

};

ActivityBase.prototype.calcAcceleratedTime = function( nT )
{
    // Handle acceleration/deceleration


    // clamp nT to permissible [0,1] range
    nT = clamp( nT, 0.0, 1.0 );

    // take acceleration/deceleration into account. if the sum
    // of nAccelerationFraction and nDecelerationFraction
    // exceeds 1.0, ignore both (that's according to SMIL spec)
    if( ( this.nAccelerationFraction > 0.0 || this.nDecelerationFraction > 0.0 ) &&
        ( this.nAccelerationFraction + this.nDecelerationFraction <= 1.0 ) )
    {
        var nC = 1.0 - 0.5*this.nAccelerationFraction - 0.5*this.nDecelerationFraction;

        // this variable accumulates the new time value
        var nTPrime = 0.0;

        if( nT < this.nAccelerationFraction )
        {
            nTPrime += 0.5 * nT * nT / this.nAccelerationFraction; // partial first interval
        }
        else
        {
            nTPrime += 0.5 * this.nAccelerationFraction; // full first interval

            if( nT <= ( 1.0 - this.nDecelerationFraction ) )
            {
                nTPrime += nT - this.nAccelerationFraction; // partial second interval
            }
            else
            {
                nTPrime += 1.0 - this.nAccelerationFraction - this.nDecelerationFraction; // full second interval

                var nTRelative = nT - 1.0 + this.nDecelerationFraction;

                nTPrime += nTRelative - 0.5*nTRelative*nTRelative / this.nDecelerationFraction;
            }
        }

        // normalize, and assign to work variable
        nT = nTPrime / nC;

    }
    return nT;
};

ActivityBase.prototype.getEventQueue = function()
{
    return this.aTimerEventQueue;
};

ActivityBase.prototype.getTargetElement = function()
{
    return this.aTargetElement;
};

ActivityBase.prototype.isRepeatCountValid = function()
{
    return !!this.nRepeats; // first ! convert to bool
};

ActivityBase.prototype.getRepeatCount = function()
{
    return this.nRepeats;
};

ActivityBase.prototype.isAutoReverse = function()
{
    return this.bAutoReverse;
};

ActivityBase.prototype.end = function()
{
    if( !this.isActive() || this.isDisposed() )
        return;

    // assure animation is started:
    if( this.bFirstPerformCall )
    {
        this.bFirstPerformCall = false;
        // notify derived classes that we're starting now
        this.startAnimation();
    }

    this.performEnd();
    this.endAnimation();
    this.endActivity();
};

ActivityBase.prototype.performEnd = function()
{
    throw ( 'ActivityBase.performEnd: abstract method invoked' );
};




function DiscreteActivityBase( aCommonParamSet )
{
    DiscreteActivityBase.superclass.constructor.call( this, aCommonParamSet );

    this.aOriginalWakeupEvent = aCommonParamSet.aWakeupEvent;
    this.aOriginalWakeupEvent.setActivity( this );
    this.aWakeupEvent = this.aOriginalWakeupEvent;
    this.aWakeupEvent = aCommonParamSet.aWakeupEvent;
    this.aDiscreteTimes = aCommonParamSet.aDiscreteTimes;
    // Simple duration of activity
    this.nMinSimpleDuration = aCommonParamSet.nMinDuration;
    // Actual number of frames shown until now.
    this.nCurrPerformCalls = 0;
}
extend( DiscreteActivityBase, ActivityBase );


DiscreteActivityBase.prototype.activate = function( aEndElement )
{
    DiscreteActivityBase.superclass.activate.call( this, aEndElement );

    this.aWakeupEvent = this.aOriginalWakeupEvent;
    this.aWakeupEvent.setNextTimeout( 0 );
    this.nCurrPerformCalls = 0;
};

DiscreteActivityBase.prototype.startAnimation = function()
{
    this.aWakeupEvent.start();
};

DiscreteActivityBase.prototype.calcFrameIndex = function( nCurrCalls, nVectorSize )
{
    if( this.isAutoReverse() )
    {
        // every full repeat run consists of one
        // forward and one backward traversal.
        var nFrameIndex = nCurrCalls % (2 * nVectorSize);

        // nFrameIndex values >= nVectorSize belong to
        // the backward traversal
        if( nFrameIndex >= nVectorSize )
            nFrameIndex = 2*nVectorSize - nFrameIndex; // invert sweep

        return nFrameIndex;
    }
    else
    {
        return nCurrCalls % nVectorSize;
    }
};

DiscreteActivityBase.prototype.calcRepeatCount = function( nCurrCalls, nVectorSize )
{
    if( this.isAutoReverse() )
    {
        return Math.floor( nCurrCalls / (2*nVectorSize) ); // we've got 2 cycles per repeat
    }
    else
    {
        return Math.floor( nCurrCalls / nVectorSize );
    }
};

DiscreteActivityBase.prototype.performDiscreteHook = function( /*nFrame, nRepeatCount*/ )
{
    throw ( 'DiscreteActivityBase.performDiscreteHook: abstract method invoked' );
};

DiscreteActivityBase.prototype.perform = function()
{
    // call base class, for start() calls and end handling
    if( !SimpleContinuousActivityBase.superclass.perform.call( this ) )
        return false; // done, we're ended

    var nVectorSize = this.aDiscreteTimes.length;

    var nFrameIndex = this.calcFrameIndex(this.nCurrPerformCalls, nVectorSize);
    var nRepeatCount = this.calcRepeatCount( this.nCurrPerformCalls, nVectorSize );
    this.performDiscreteHook( nFrameIndex, nRepeatCount );

    // one more frame successfully performed
    ++this.nCurrPerformCalls;

    // calc currently reached repeat count
    var nCurrRepeat = this.nCurrPerformCalls / nVectorSize;

    // if auto-reverse is specified, halve the
    // effective repeat count, since we pass every
    // repeat run twice: once forward, once backward.
    if( this.isAutoReverse() )
        nCurrRepeat /= 2;

    // schedule next frame, if either repeat is indefinite
    // (repeat forever), or we've not yet reached the requested
    // repeat count
    if( !this.isRepeatCountValid() || nCurrRepeat < this.getRepeatCount() )
    {
        // add wake-up event to queue (modulo vector size, to cope with repeats).

        // repeat is handled locally, only apply acceleration/deceleration.
        // Scale time vector with simple duration, offset with full repeat
        // times.

        // Note that calcAcceleratedTime() is only applied to the current repeat's value,
        // not to the total resulting time. This is in accordance with the SMIL spec.

        nFrameIndex = this.calcFrameIndex(this.nCurrPerformCalls, nVectorSize);
        var nCurrentRepeatTime = this.aDiscreteTimes[nFrameIndex];
        nRepeatCount = this.calcRepeatCount( this.nCurrPerformCalls, nVectorSize );
        var nNextTimeout = this.nMinSimpleDuration * ( nRepeatCount + this.calcAcceleratedTime( nCurrentRepeatTime ) );
        this.aWakeupEvent.setNextTimeout( nNextTimeout );

        this.getEventQueue().addEvent( this.aWakeupEvent );
    }
    else
    {
        // release event reference (relation to wake up event is circular!)
        this.aWakeupEvent = null;

        // done with this activity
        this.endActivity();
    }

    return false; // remove from queue, will be added back by the wakeup event.
};

DiscreteActivityBase.prototype.dispose = function()
{
    // dispose event
    if( this.aWakeupEvent )
        this.aWakeupEvent.dispose();

    // release references
    this.aWakeupEvent = null;

    DiscreteActivityBase.superclass.dispose.call(this);
};




function SimpleContinuousActivityBase( aCommonParamSet )
{
    SimpleContinuousActivityBase.superclass.constructor.call( this, aCommonParamSet );

    // Time elapsed since activity started
    this.aTimer = new ElapsedTime( aCommonParamSet.aActivityQueue.getTimer() );
    // Simple duration of activity
    this.nMinSimpleDuration = aCommonParamSet.nMinDuration;
    // Minimal number of frames to show
    this.nMinNumberOfFrames = aCommonParamSet.nMinNumberOfFrames;
    // Actual number of frames shown until now.
    this.nCurrPerformCalls = 0;

}
extend( SimpleContinuousActivityBase, ActivityBase );


SimpleContinuousActivityBase.prototype.startAnimation = function()
{
    // init timer. We measure animation time only when we're
    // actually started.
    this.aTimer.reset();
};

SimpleContinuousActivityBase.prototype.calcTimeLag = function()
{
    SimpleContinuousActivityBase.superclass.calcTimeLag.call( this );

    if( !this.isActive() )
        return 0.0;

    // retrieve locally elapsed time
    var nCurrElapsedTime = this.aTimer.getElapsedTime();

    // go to great length to ensure a proper animation
    // run. Since we don't know how often we will be called
    // here, try to spread the animator calls uniquely over
    // the [0,1] parameter range. Be aware of the fact that
    // perform will be called at least mnMinNumberOfTurns
    // times.

    // fraction of time elapsed
    var nFractionElapsedTime = nCurrElapsedTime / this.nMinSimpleDuration;

    // fraction of minimum calls performed
    var nFractionRequiredCalls = this.nCurrPerformCalls / this.nMinNumberOfFrames;

    // okay, so now, the decision is easy:
    //
    // If the fraction of time elapsed is smaller than the
    // number of calls required to be performed, then we calc
    // the position on the animation range according to
    // elapsed time. That is, we're so to say ahead of time.
    //
    // In contrary, if the fraction of time elapsed is larger,
    // then we're lagging, and we thus calc the position on
    // the animation time line according to the fraction of
    // calls performed. Thus, the animation is forced to slow
    // down, and take the required minimal number of steps,
    // sufficiently equally distributed across the animation
    // time line.

    if( nFractionElapsedTime < nFractionRequiredCalls )
    {
        return 0.0;
    }
    else
    {
        // lag global time, so all other animations lag, too:
        return ( ( nFractionElapsedTime - nFractionRequiredCalls ) * this.nMinSimpleDuration );
    }
};

SimpleContinuousActivityBase.prototype.perform = function()
{
    // call base class, for start() calls and end handling
    if( !SimpleContinuousActivityBase.superclass.perform.call( this ) )
        return false; // done, we're ended

    // get relative animation position
    var nCurrElapsedTime = this.aTimer.getElapsedTime();
    var nT = nCurrElapsedTime / this.nMinSimpleDuration;


    // one of the stop criteria reached?

    // will be set to true below, if one of the termination criteria matched.
    var bActivityEnding = false;

    if( this.isRepeatCountValid() )
    {
        // Finite duration case

        // When we've autoreverse on, the repeat count doubles
        var nRepeatCount = this.getRepeatCount();
        var nEffectiveRepeat = this.isAutoReverse() ? 2.0 * nRepeatCount : nRepeatCount;

        // time (or frame count) elapsed?
        if( nEffectiveRepeat <= nT )
        {
            // Ok done for now. Will not exit right here,
            // to give animation the chance to render the last
            // frame below
            bActivityEnding = true;

            // clamp animation to max permissible value
            nT = nEffectiveRepeat;
        }
    }


    // need to do auto-reverse?

    var nRepeats;
    var nRelativeSimpleTime;
    // TODO(Q3): Refactor this mess
    if( this.isAutoReverse() )
    {
        // divert active duration into repeat and
        // fractional part.
        nRepeats = Math.floor( nT );
        var nFractionalActiveDuration =  nT - nRepeats;

        // for auto-reverse, map ranges [1,2), [3,4), ...
        // to ranges [0,1), [1,2), etc.
        if( nRepeats % 2 )
        {
            // we're in an odd range, reverse sweep
            nRelativeSimpleTime = 1.0 - nFractionalActiveDuration;
        }
        else
        {
            // we're in an even range, pass on as is
            nRelativeSimpleTime = nFractionalActiveDuration;
        }

        // effective repeat count for autoreverse is half of
        // the input time's value (each run of an autoreverse
        // cycle is half of a repeat)
        nRepeats /= 2;
    }
    else
    {
        // determine repeat

        // calc simple time and number of repeats from nT
        // Now, that's easy, since the fractional part of
        // nT gives the relative simple time, and the
        // integer part the number of full repeats:
        nRepeats = Math.floor( nT );
        nRelativeSimpleTime = nT - nRepeats;

        // clamp repeats to max permissible value (maRepeats.getValue() - 1.0)
        if( this.isRepeatCountValid() && ( nRepeats >= this.getRepeatCount() ) )
        {
            // Note that this code here only gets
            // triggered if this.nRepeats is an
            // _integer_. Otherwise, nRepeats will never
            // reach nor exceed
            // maRepeats.getValue(). Thus, the code below
            // does not need to handle cases of fractional
            // repeats, and can always assume that a full
            // animation run has ended (with
            // nRelativeSimpleTime = 1.0 for
            // non-autoreversed activities).

            // with modf, nRelativeSimpleTime will never
            // become 1.0, since nRepeats is incremented and
            // nRelativeSimpleTime set to 0.0 then.
            //
            // For the animation to reach its final value,
            // nRepeats must although become this.nRepeats - 1.0,
            // and nRelativeSimpleTime = 1.0.
            nRelativeSimpleTime = 1.0;
            nRepeats -= 1.0;
        }
    }


    // actually perform something

    this.simplePerform( nRelativeSimpleTime, nRepeats );

    // delayed endActivity() call from end condition check
    // below. Issued after the simplePerform() call above, to
    // give animations the chance to correctly reach the
    // animation end value, without spurious bail-outs because
    // of isActive() returning false.
    if( bActivityEnding )
        this.endActivity();

    // one more frame successfully performed
    ++this.nCurrPerformCalls;

    return this.isActive();
};

SimpleContinuousActivityBase.prototype.simplePerform = function( /*nSimpleTime, nRepeatCount*/ )
{
    throw ( 'SimpleContinuousActivityBase.simplePerform: abstract method invoked' );
};




function ContinuousKeyTimeActivityBase( aCommonParamSet )
{
    var nSize = aCommonParamSet.aDiscreteTimes.length;
    assert( nSize > 1,
            'ContinuousKeyTimeActivityBase constructor: assertion (aDiscreteTimes.length > 1) failed' );

    assert( aCommonParamSet.aDiscreteTimes[0] == 0.0,
            'ContinuousKeyTimeActivityBase constructor: assertion (aDiscreteTimes.front() == 0.0) failed' );

    assert( aCommonParamSet.aDiscreteTimes[ nSize - 1 ] <= 1.0,
            'ContinuousKeyTimeActivityBase constructor: assertion (aDiscreteTimes.back() <= 1.0) failed' );

    ContinuousKeyTimeActivityBase.superclass.constructor.call( this, aCommonParamSet );

    this.aLerper = new KeyStopLerp( aCommonParamSet.aDiscreteTimes );
}
extend( ContinuousKeyTimeActivityBase, SimpleContinuousActivityBase );


ContinuousKeyTimeActivityBase.prototype.activate = function( aEndElement )
{
    ContinuousKeyTimeActivityBase.superclass.activate.call( this, aEndElement );

    this.aLerper.reset();
};

ContinuousKeyTimeActivityBase.prototype.performContinuousHook = function( /*nIndex, nFractionalIndex, nRepeatCount*/ )
{
    throw ( 'ContinuousKeyTimeActivityBase.performContinuousHook: abstract method invoked' );
};

ContinuousKeyTimeActivityBase.prototype.simplePerform = function( nSimpleTime, nRepeatCount )
{
    var nAlpha = this.calcAcceleratedTime( nSimpleTime );

    var aLerpResult = this.aLerper.lerp( nAlpha );

    this.performContinuousHook( aLerpResult.nIndex, aLerpResult.nLerp, nRepeatCount );
};




function ContinuousActivityBase( aCommonParamSet )
{
    ContinuousActivityBase.superclass.constructor.call( this, aCommonParamSet );

}
extend( ContinuousActivityBase, SimpleContinuousActivityBase );


ContinuousActivityBase.prototype.performContinuousHook = function( /*nModifiedTime, nRepeatCount*/ )
{
    throw ( 'ContinuousActivityBase.performContinuousHook: abstract method invoked' );
};

ContinuousActivityBase.prototype.simplePerform = function( nSimpleTime, nRepeatCount )
{
    this.performContinuousHook( this.calcAcceleratedTime( nSimpleTime ), nRepeatCount );
};




function SimpleActivity( aCommonParamSet, aNumberAnimation, eDirection )
{
    assert( ( eDirection == BACKWARD ) || ( eDirection == FORWARD ),
            'SimpleActivity constructor: animation direction is not valid' );

    assert( aNumberAnimation, 'SimpleActivity constructor: animation object is not valid' );

    SimpleActivity.superclass.constructor.call( this, aCommonParamSet );

    this.aAnimation = aNumberAnimation;
    this.nDirection = ( eDirection == FORWARD ) ? 1.0 : 0.0;
}
extend( SimpleActivity, ContinuousActivityBase );


SimpleActivity.prototype.startAnimation = function()
{
    if( this.isDisposed() || !this.aAnimation )
        return;

    ANIMDBG.print( 'SimpleActivity.startAnimation invoked' );
    SimpleActivity.superclass.startAnimation.call( this );

    // start animation
    this.aAnimation.start( this.getTargetElement() );
};

SimpleActivity.prototype.endAnimation = function()
{
    if( this.aAnimation )
        this.aAnimation.end();

};

SimpleActivity.prototype.performContinuousHook = function( nModifiedTime /*, nRepeatCount*/ )
{
    // nRepeatCount is not used

    if( this.isDisposed() || !this.aAnimation )
        return;

    var nT = 1.0 - this.nDirection + nModifiedTime * ( 2.0*this.nDirection - 1.0 );
    this.aAnimation.perform( nT );
};

SimpleActivity.prototype.performEnd = function()
{
    if( this.aAnimation )
        this.aAnimation.perform( this.nDirection );
};




//  FromToByActivity< BaseType > template class


function FromToByActivityTemplate( BaseType ) // template parameter
{

    function FromToByActivity( aFromValue, aToValue, aByValue,
                               aActivityParamSet, aAnimation,
                               aInterpolator, aOperatorSet, bAccumulate )
    {
        assert( aAnimation, 'FromToByActivity constructor: invalid animation object' );
        assert( ( aToValue != undefined ) || ( aByValue != undefined ),
                'FromToByActivity constructor: one of aToValue or aByValue must be valid' );

        FromToByActivity.superclass.constructor.call( this, aActivityParamSet );

        this.aFrom = aFromValue;
        this.aTo = aToValue;
        this.aBy = aByValue;
        this.aStartValue = null;
        this.aEndValue = null;
        this.aPreviousValue = null;
        this.aStartInterpolationValue = null;
        this.aAnimation = aAnimation;
        this.aInterpolator = aInterpolator;
        this.equal = aOperatorSet.equal;
        this.add = aOperatorSet.add;
        this.scale = aOperatorSet.scale;
        this.bDynamicStartValue = false;
        this.nIteration = 0;
        this.bCumulative = bAccumulate;
        this.aFormula = aActivityParamSet.aFormula;
    }
    extend( FromToByActivity, BaseType );

    FromToByActivity.prototype.initAnimatedElement = function()
    {
        if( this.aAnimation && this.aFrom )
        {
            var aValue = this.aFormula ? this.aFormula( this.aFrom ) : this.aFrom;
            this.aAnimation.perform(aValue);
        }
    };

    FromToByActivity.prototype.startAnimation = function()
    {
        if( this.isDisposed() || !this.aAnimation  )
        {
            log( 'FromToByActivity.startAnimation: activity disposed or not valid animation' );
            return;
        }

        FromToByActivity.superclass.startAnimation.call( this );

        this.aAnimation.start( this.getTargetElement() );


        var aAnimationStartValue = this.aAnimation.getUnderlyingValue();

        // first of all, determine general type of
        // animation, by inspecting which of the FromToBy values
        // are actually valid.
        // See http://www.w3.org/TR/smil20/animation.html#AnimationNS-FromToBy
        // for a definition
        if( this.aFrom )
        {
            // From-to or From-by animation. According to
            // SMIL spec, the To value takes precedence
            // over the By value, if both are specified
            if( this.aTo )
            {
                // From-To animation
                this.aStartValue = this.aFrom;
                this.aEndValue = this.aTo;
            }
            else if( this.aBy )
            {
                // From-By animation
                this.aStartValue = this.aFrom;

                this.aEndValue = this.add( this.aStartValue, this.aBy );
            }
        }
        else
        {
            this.aStartValue = aAnimationStartValue;
            this.aStartInterpolationValue = this.aStartValue;

            // By or To animation. According to SMIL spec,
            // the To value takes precedence over the By
            // value, if both are specified
            if( this.aTo )
            {
                // To animation

                // According to the SMIL spec
                // (http://www.w3.org/TR/smil20/animation.html#animationNS-ToAnimation),
                // the to animation interpolates between
                // the _running_ underlying value and the to value (as the end value)
                this.bDynamicStartValue = true;
                this.aPreviousValue = this.aStartValue;
                this.aEndValue = this.aTo;
            }
            else if( this.aBy )
            {
                // By animation
                this.aStartValue = aAnimationStartValue;

                this.aEndValue = this.add( this.aStartValue, this.aBy );
            }
        }

        ANIMDBG.print( 'FromToByActivity.startAnimation: aStartValue = ' + this.aStartValue + ', aEndValue = ' + this.aEndValue );
    };

    FromToByActivity.prototype.endAnimation = function()
    {
        if( this.aAnimation )
            this.aAnimation.end();
    };

    // perform hook override for ContinuousActivityBase
    FromToByActivity.prototype.performContinuousHook = function( nModifiedTime, nRepeatCount )
    {
        if( this.isDisposed() || !this.aAnimation  )
        {
            log( 'FromToByActivity.performContinuousHook: activity disposed or not valid animation' );
            return;
        }


        // According to SMIL 3.0 spec 'to' animation if no other (lower priority)
        // animations are active or frozen then a simple interpolation is performed.
        // That is, the start interpolation value is constant while the animation
        // is running, and is equal to the underlying value retrieved when
        // the animation start.
        // However if another animation is manipulating the underlying value,
        // the 'to' animation will initially add to the effect of the lower priority
        // animation, and increasingly dominate it as it nears the end of the
        // simple duration, eventually overriding it completely.
        // That is, each time the underlying value is changed between two
        // computations of the animation function the new underlying value is used
        // as start value for the interpolation.
        // See:
        // http://www.w3.org/TR/SMIL3/smil-animation.html#animationNS-ToAnimation
        // (Figure 6 - Effect of Additive to animation example)
        // Moreover when a 'to' animation is repeated, at each new iteration
        // the start interpolation value is reset to the underlying value
        // of the animated property when the animation started,
        // as it is shown in the example provided by the SMIL 3.0 spec.
        // This is exactly as Firefox performs SVG 'to' animations.
        if( this.bDynamicStartValue )
        {
            if( this.nIteration != nRepeatCount )
            {
                this.nIteration = nRepeatCount;
                this.aStartInterpolationValue =  this.aStartValue;
            }
            else
            {
                var aActualValue = this.aAnimation.getUnderlyingValue();
                if( !this.equal( aActualValue, this.aPreviousValue ) )
                    this.aStartInterpolationValue = aActualValue;
            }
        }

        var aValue = this.aInterpolator( this.aStartInterpolationValue,
                                         this.aEndValue, nModifiedTime );

        // According to the SMIL spec:
        // Because 'to' animation is defined in terms of absolute values of
        // the target attribute, cumulative animation is not defined.
        if( this.bCumulative && !this.bDynamicStartValue )
        {
            // aValue = this.aEndValue * nRepeatCount + aValue;
            aValue = this.add( this.scale( nRepeatCount, this.aEndValue ), aValue );
        }

        aValue = this.aFormula ? this.aFormula( aValue ) : aValue;
        this.aAnimation.perform( aValue );

        if( this.bDynamicStartValue )
        {
            this.aPreviousValue = this.aAnimation.getUnderlyingValue();
        }

    };

    // perform hook override for DiscreteActivityBase
    FromToByActivity.prototype.performDiscreteHook = function( /*nFrame, nRepeatCount*/ )
    {
        if (this.isDisposed() || !this.aAnimation) {
            log('FromToByActivity.performDiscreteHook: activity disposed or not valid animation');
            return;
        }
    };

    FromToByActivity.prototype.performEnd = function()
    {
        if( this.aAnimation )
        {
            var aValue = this.isAutoReverse() ? this.aStartValue : this.aEndValue;
            aValue = this.aFormula ? this.aFormula( aValue ) : aValue;
            this.aAnimation.perform( aValue );
        }
    };

    FromToByActivity.prototype.dispose = function()
    {
        FromToByActivity.superclass.dispose.call( this );
    };


    return FromToByActivity;
}


// FromToByActivity< ContinuousActivityBase > instantiation
var LinearFromToByActivity = instantiate( FromToByActivityTemplate, ContinuousActivityBase );
// FromToByActivity< DiscreteActivityBase > instantiation
var DiscreteFromToByActivity = instantiate( FromToByActivityTemplate, DiscreteActivityBase );




//  ValueListActivity< BaseType > template class


function  ValueListActivityTemplate( BaseType ) // template parameter
{

    function ValueListActivity( aValueList, aActivityParamSet,
                                aAnimation, aInterpolator,
                                aOperatorSet, bAccumulate )
    {
        assert( aAnimation, 'ValueListActivity constructor: invalid animation object' );
        assert( aValueList.length != 0, 'ValueListActivity: value list is empty' );

        ValueListActivity.superclass.constructor.call( this, aActivityParamSet );

        this.aValueList = aValueList;
        this.aAnimation = aAnimation;
        this.aInterpolator = aInterpolator;
        this.add = aOperatorSet.add;
        this.scale = aOperatorSet.scale;
        this.bCumulative = bAccumulate;
        this.aLastValue = this.aValueList[ this.aValueList.length - 1 ];
        this.aFormula = aActivityParamSet.aFormula;
    }
    extend( ValueListActivity, BaseType );

    ValueListActivity.prototype.activate = function( aEndEvent )
    {
        ValueListActivity.superclass.activate.call( this, aEndEvent );
        for( var i = 0; i < this.aValueList.length; ++i )
        {
            ANIMDBG.print( 'createValueListActivity: value[' + i + '] = ' + this.aValueList[i] );
        }
    };

    ValueListActivity.prototype.initAnimatedElement = function()
    {
        if( this.aAnimation )
        {
            var aValue = this.aValueList[0];
            aValue = this.aFormula ? this.aFormula( aValue ) : aValue;
            this.aAnimation.perform(aValue);
        }
    };

    ValueListActivity.prototype.startAnimation = function()
    {
        if( this.isDisposed() || !this.aAnimation  )
        {
            log( 'ValueListActivity.startAnimation: activity disposed or not valid animation' );
            return;
        }

        ValueListActivity.superclass.startAnimation.call( this );

        this.aAnimation.start( this.getTargetElement() );
    };

    ValueListActivity.prototype.endAnimation = function()
    {
        if( this.aAnimation )
            this.aAnimation.end();
    };

    // perform hook override for ContinuousKeyTimeActivityBase base
    ValueListActivity.prototype.performContinuousHook = function( nIndex, nFractionalIndex, nRepeatCount )
    {
        if( this.isDisposed() || !this.aAnimation  )
        {
            log( 'ValueListActivity.performContinuousHook: activity disposed or not valid animation' );
            return;
        }

        assert( ( nIndex + 1 ) < this.aValueList.length,
                'ValueListActivity.performContinuousHook: assertion (nIndex + 1 < this.aValueList.length) failed' );

        // interpolate between nIndex and nIndex+1 values

        var aValue = this.aInterpolator( this.aValueList[ nIndex ],
                                         this.aValueList[ nIndex+1 ],
                                         nFractionalIndex );

        if( this.bCumulative )
        {
            //aValue = aValue + nRepeatCount * this.aLastValue;
            aValue = this.add( aValue, this.scale( nRepeatCount, this.aLastValue ) );
        }

        aValue = this.aFormula ? this.aFormula( aValue ) : aValue;
        this.aAnimation.perform( aValue );
    };

    // perform hook override for DiscreteActivityBase base
    ValueListActivity.prototype.performDiscreteHook = function( nFrame, nRepeatCount )
    {
        if( this.isDisposed() || !this.aAnimation  )
        {
            log( 'ValueListActivity.performDiscreteHook: activity disposed or not valid animation' );
            return;
        }

        assert( nFrame < this.aValueList.length,
               'ValueListActivity.performDiscreteHook: assertion ( nFrame < this.aValueList.length) failed' );

        // this is discrete, thus no lerp here.
        var aValue = this.aValueList[nFrame];

        if( this.bCumulative )
        {
            aValue = this.add( aValue, this.scale( nRepeatCount, this.aLastValue ) );
            // for numbers:   aValue = aValue + nRepeatCount * this.aLastValue;
            // for enums, bools or strings:   aValue = aValue;
        }

        aValue = this.aFormula ? this.aFormula( aValue ) : aValue;
        this.aAnimation.perform( aValue );
    };

    ValueListActivity.prototype.performEnd = function()
    {
        if( this.aAnimation )
        {
            var aValue = this.aFormula ? this.aFormula( this.aLastValue ) : this.aLastValue;
            this.aAnimation.perform( aValue );
        }
    };

    ValueListActivity.prototype.dispose = function()
    {
        ValueListActivity.superclass.dispose.call( this );
    };


    return ValueListActivity;
}


//  ValueListActivity< ContinuousKeyTimeActivityBase > instantiation
var LinearValueListActivity = instantiate( ValueListActivityTemplate, ContinuousKeyTimeActivityBase );
//  ValueListActivity< DiscreteActivityBase > instantiation
var DiscreteValueListActivity = instantiate( ValueListActivityTemplate, DiscreteActivityBase );



/**********************************************************************************************
 *      Activity Factory
 **********************************************************************************************/


function createActivity( aActivityParamSet, aAnimationNode, aAnimation, aInterpolator )
{
    var eCalcMode = aAnimationNode.getCalcMode();

    var sAttributeName = aAnimationNode.getAttributeName();
    var aAttributeProp = aAttributeMap[ sAttributeName ];

    var eValueType = aAttributeProp[ 'type' ];
    var eValueSubtype = aAttributeProp[ 'subtype' ];

    // do we need to get an interpolator ?
    if( ! aInterpolator )
    {
        aInterpolator = aInterpolatorHandler.getInterpolator( eCalcMode,
                                                              eValueType,
                                                              eValueSubtype );
    }

    // is it cumulative ?
    var bAccumulate = ( aAnimationNode.getAccumulate() === ACCUMULATE_MODE_SUM )
                            && !( eValueType === BOOL_PROPERTY ||
                                  eValueType === STRING_PROPERTY ||
                                  eValueType === ENUM_PROPERTY );

    if( aAnimationNode.getFormula() )
    {
        var sFormula =  aAnimationNode.getFormula();
        var reMath = /abs|sqrt|asin|acos|atan|sin|cos|tan|exp|log|min|max/g;
        sFormula = sFormula.replace(reMath, 'Math.$&');
        sFormula = sFormula.replace(/pi(?!\w)/g, 'Math.PI');
        sFormula = sFormula.replace(/e(?!\w)/g, 'Math.E');
        sFormula = sFormula.replace(/\$/g, '__PARAM0__');

        var aAnimatedElement = aAnimationNode.getAnimatedElement();
        var aBBox = aAnimatedElement.getBaseBBox();

        // the following variable are used for evaluating sFormula
        /* eslint-disable no-unused-vars */
        var width = aBBox.width / aActivityParamSet.nSlideWidth;
        var height = aBBox.height / aActivityParamSet.nSlideHeight;
        var x = ( aBBox.x + aBBox.width / 2 ) / aActivityParamSet.nSlideWidth;
        var y = ( aBBox.y + aBBox.height / 2 ) / aActivityParamSet.nSlideHeight;

        aActivityParamSet.aFormula = function( __PARAM0__ ) {

            return eval(sFormula);
        };
        /* eslint-enable no-unused-vars */
    }

    aActivityParamSet.aDiscreteTimes = aAnimationNode.getKeyTimes();

    // do we have a value list ?
    var aValueSet = aAnimationNode.getValues();
    var nValueSetSize = aValueSet.length;

    if( nValueSetSize != 0 )
    {
        // Value list activity

        if( aActivityParamSet.aDiscreteTimes.length == 0 )
        {
            for( var i = 0; i < nValueSetSize; ++i )
                aActivityParamSet.aDiscreteTimes[i].push( i / nValueSetSize );
        }

        switch( eCalcMode )
        {
            case CALC_MODE_DISCRETE:
                aActivityParamSet.aWakeupEvent =
                        new WakeupEvent( aActivityParamSet.aTimerEventQueue.getTimer(),
                                         aActivityParamSet.aActivityQueue );

                return createValueListActivity( aActivityParamSet,
                                                aAnimationNode,
                                                aAnimation,
                                                aInterpolator,
                                                DiscreteValueListActivity,
                                                bAccumulate,
                                                eValueType );

            default:
                log( 'createActivity: unexpected calculation mode: ' + eCalcMode );
                // FALLTHROUGH intended
            case CALC_MODE_PACED :
            case CALC_MODE_SPLINE :
            case CALC_MODE_LINEAR:
                return createValueListActivity( aActivityParamSet,
                                                aAnimationNode,
                                                aAnimation,
                                                aInterpolator,
                                                LinearValueListActivity,
                                                bAccumulate,
                                                eValueType );
        }
    }
    else
    {
        // FromToBy activity
        switch( eCalcMode )
        {
            case CALC_MODE_DISCRETE:
                log( 'createActivity: discrete calculation case not yet implemented' );
                aActivityParamSet.aWakeupEvent =
                        new WakeupEvent( aActivityParamSet.aTimerEventQueue.getTimer(),
                                         aActivityParamSet.aActivityQueue );
                return createFromToByActivity(  aActivityParamSet,
                                                aAnimationNode,
                                                aAnimation,
                                                aInterpolator,
                                                DiscreteFromToByActivity,
                                                bAccumulate,
                                                eValueType );

            default:
                log( 'createActivity: unexpected calculation mode: ' + eCalcMode );
                // FALLTHROUGH intended
            case CALC_MODE_PACED :
            case CALC_MODE_SPLINE :
            case CALC_MODE_LINEAR:
                return createFromToByActivity(  aActivityParamSet,
                                                aAnimationNode,
                                                aAnimation,
                                                aInterpolator,
                                                LinearFromToByActivity,
                                                bAccumulate,
                                                eValueType );
        }
    }
}




function createValueListActivity( aActivityParamSet, aAnimationNode, aAnimation,
                                  aInterpolator, ClassTemplateInstance, bAccumulate, eValueType )
{
    var aAnimatedElement = aAnimationNode.getAnimatedElement();
    var aOperatorSet = aOperatorSetMap[ eValueType ];
    assert( aOperatorSet, 'createValueListActivity: no operator set found' );

    var aValueSet = aAnimationNode.getValues();

    var aValueList = [];

    extractAttributeValues( eValueType,
                            aValueList,
                            aValueSet,
                            aAnimatedElement.getBaseBBox(),
                            aActivityParamSet.nSlideWidth,
                            aActivityParamSet.nSlideHeight );

    for( var i = 0; i < aValueList.length; ++i )
    {
        ANIMDBG.print( 'createValueListActivity: value[' + i + '] = ' + aValueList[i] );
    }

    return new ClassTemplateInstance( aValueList, aActivityParamSet, aAnimation,
                                      aInterpolator, aOperatorSet, bAccumulate );
}




function createFromToByActivity( aActivityParamSet, aAnimationNode, aAnimation,
                                 aInterpolator, ClassTemplateInstance, bAccumulate, eValueType )
{

    var aAnimatedElement = aAnimationNode.getAnimatedElement();
    var aOperatorSet = aOperatorSetMap[ eValueType ];
    assert( aOperatorSet, 'createFromToByActivity: no operator set found' );

    var aValueSet = [];
    aValueSet[0] = aAnimationNode.getFromValue();
    aValueSet[1] = aAnimationNode.getToValue();
    aValueSet[2] = aAnimationNode.getByValue();

    ANIMDBG.print( 'createFromToByActivity: value type: ' + aValueTypeOutMap[eValueType] +
                    ', aFrom = ' + aValueSet[0] +
                    ', aTo = ' + aValueSet[1] +
                    ', aBy = ' + aValueSet[2] );

    var aValueList = [];

    extractAttributeValues( eValueType,
                            aValueList,
                            aValueSet,
                            aAnimatedElement.getBaseBBox(),
                            aActivityParamSet.nSlideWidth,
                            aActivityParamSet.nSlideHeight );

    ANIMDBG.print( 'createFromToByActivity: ' +
                    ', aFrom = ' + aValueList[0] +
                    ', aTo = ' + aValueList[1] +
                    ', aBy = ' + aValueList[2] );

    return new ClassTemplateInstance( aValueList[0], aValueList[1], aValueList[2],
                                      aActivityParamSet, aAnimation,
                                      aInterpolator, aOperatorSet, bAccumulate );
}



function extractAttributeValues( eValueType, aValueList, aValueSet, aBBox, nSlideWidth, nSlideHeight )
{
    var i;
    switch( eValueType )
    {
        case NUMBER_PROPERTY :
            evalValuesAttribute( aValueList, aValueSet, aBBox, nSlideWidth, nSlideHeight );
            break;
        case BOOL_PROPERTY :
            for( i = 0; i < aValueSet.length; ++i )
            {
                var aValue = booleanParser( aValueSet[i] );
                aValueList.push( aValue );
            }
            break;
        case STRING_PROPERTY :
            for( i = 0; i < aValueSet.length; ++i )
            {
                aValueList.push( aValueSet[i] );
            }
            break;
        case ENUM_PROPERTY :
            for( i = 0; i < aValueSet.length; ++i )
            {
                aValueList.push( aValueSet[i] );
            }
            break;
        case COLOR_PROPERTY :
            for( i = 0; i < aValueSet.length; ++i )
            {
                aValue = colorParser( aValueSet[i] );
                aValueList.push( aValue );
            }
            break;
        case TUPLE_NUMBER_PROPERTY :
            for( i = 0; i < aValueSet.length; ++i )
            {
                if( typeof aValueSet[i] === 'string' )
                {
                    var aTuple = aValueSet[i].split(',');
                    aValue = [];
                    evalValuesAttribute(aValue, aTuple, aBBox, nSlideWidth, nSlideHeight);
                    aValueList.push(aValue);
                }
                else
                {
                    aValueList.push( undefined );
                }
            }
            break;
        default:
            log( 'createValueListActivity: unexpected value type: ' + eValueType );
    }

}


function evalValuesAttribute( aValueList, aValueSet, aBBox, nSlideWidth, nSlideHeight )
{
    // the following variables are used for evaluating sValue later
    /* eslint-disable no-unused-vars */
    var width = aBBox.width / nSlideWidth;
    var height = aBBox.height / nSlideHeight;
    var x = ( aBBox.x + aBBox.width / 2 ) / nSlideWidth;
    var y = ( aBBox.y + aBBox.height / 2 ) / nSlideHeight;
    /* eslint-enable no-unused-vars */

    var reMath = /abs|sqrt|asin|acos|atan|sin|cos|tan|exp|log|min|max/g;

    for( var i = 0; i < aValueSet.length; ++i )
    {
        var sValue = aValueSet[i];
        if(sValue)
        {
            sValue = sValue.replace(reMath, 'Math.$&');
            sValue = sValue.replace(/pi(?!\w)/g, 'Math.PI');
            sValue = sValue.replace(/e(?!\w)/g, 'Math.E');
        }
        var aValue =  eval( sValue );
        aValueList.push( aValue );
    }
}



/**********************************************************************************************
 *      SlideShow, SlideShowContext and FrameSynchronization
 **********************************************************************************************/



// direction of animation, important: not change the values!
var BACKWARD    = 0;
var FORWARD     = 1;

var MAXIMUM_FRAME_COUNT                 = 60;
var MINIMUM_TIMEOUT                     = 1.0 / MAXIMUM_FRAME_COUNT;
var MAXIMUM_TIMEOUT                     = 4.0;
var MINIMUM_FRAMES_PER_SECONDS          = 10;
var PREFERRED_FRAMES_PER_SECONDS        = 50;
var PREFERRED_FRAME_RATE                = 1.0 / PREFERRED_FRAMES_PER_SECONDS;


function Effect( nId )
{
    this.nId = ( typeof( nId ) === typeof( 1 ) ) ? nId : -1;
    this.eState = Effect.NOT_STARTED;
}
Effect.NOT_STARTED = 0;
Effect.PLAYING = 1;
Effect.ENDED = 2;

Effect.prototype.getId = function()
{
    return this.nId;
};

Effect.prototype.isMainEffect = function()
{
    return ( this.nId === -1 );
};

Effect.prototype.isPlaying = function()
{
    return ( this.eState === Effect.PLAYING );
};

Effect.prototype.isEnded = function()
{
    return ( this.eState === Effect.ENDED );
};

Effect.prototype.start = function()
{
    assert( this.eState === Effect.NOT_STARTED, 'Effect.start: wrong state.' );
    this.eState = Effect.PLAYING;
};

Effect.prototype.end = function()
{
    assert( this.eState === Effect.PLAYING, 'Effect.end: wrong state.' );
    this.eState = Effect.ENDED;
};



function SlideShow()
{
    this.aTimer = new ElapsedTime();
    this.aFrameSynchronization = new FrameSynchronization( PREFERRED_FRAME_RATE );
    this.aTimerEventQueue = new TimerEventQueue( this.aTimer );
    this.aActivityQueue = new ActivityQueue( this.aTimer );
    this.aNextEffectEventArray = null;
    this.aInteractiveAnimationSequenceMap = null;
    this.aEventMultiplexer = null;

    this.aContext = new SlideShowContext( this.aTimerEventQueue,
                                          this.aEventMultiplexer,
                                          this.aNextEffectEventArray,
                                          this.aInteractiveAnimationSequenceMap,
                                          this.aActivityQueue );
    this.bIsIdle = true;
    this.bIsEnabled = true;
    this.bNoSlideTransition = false;
    this.bIsTransitionRunning = false;

    this.nCurrentEffect = 0;
    this.bIsNextEffectRunning = false;
    this.bIsRewinding = false;
    this.bIsSkipping = false;
    this.bIsSkippingAll = false;
    this.nTotalInteractivePlayingEffects = 0;
    this.aStartedEffectList = [];
    this.aStartedEffectIndexMap = {};
    this.aStartedEffectIndexMap[ -1 ] = undefined;
    this.automaticAdvanceTimeout = null;
}

SlideShow.prototype.setSlideEvents = function( aNextEffectEventArray,
                                               aInteractiveAnimationSequenceMap,
                                               aEventMultiplexer )
{
    if( !aNextEffectEventArray )
        log( 'SlideShow.setSlideEvents: aNextEffectEventArray is not valid' );

    if( !aInteractiveAnimationSequenceMap )
        log( 'SlideShow.setSlideEvents:aInteractiveAnimationSequenceMap  is not valid' );

    if( !aEventMultiplexer )
        log( 'SlideShow.setSlideEvents: aEventMultiplexer is not valid' );

    this.aContext.aNextEffectEventArray = aNextEffectEventArray;
    this.aNextEffectEventArray = aNextEffectEventArray;
    this.aContext.aInteractiveAnimationSequenceMap = aInteractiveAnimationSequenceMap;
    this.aInteractiveAnimationSequenceMap = aInteractiveAnimationSequenceMap;
    this.aContext.aEventMultiplexer = aEventMultiplexer;
    this.aEventMultiplexer = aEventMultiplexer;
    this.nCurrentEffect = 0;
};

SlideShow.prototype.createSlideTransition = function( aSlideTransitionHandler, aLeavingSlide, aEnteringSlide, aTransitionEndEvent )
{
    if( !aEnteringSlide )
    {
        log( 'SlideShow.createSlideTransition: entering slide element is not valid.' );
        return null;
    }

    if( this.bNoSlideTransition ) return null;

    var aAnimatedLeavingSlide = null;
    if( aLeavingSlide )
        aAnimatedLeavingSlide = new AnimatedSlide( aLeavingSlide );
    var aAnimatedEnteringSlide = new AnimatedSlide( aEnteringSlide );

    var aSlideTransition = aSlideTransitionHandler.createSlideTransition( aAnimatedLeavingSlide, aAnimatedEnteringSlide );
    if( !aSlideTransition ) return null;

    // compute duration
    var nDuration = 0.001;
    if( aSlideTransitionHandler.getDuration().isValue() )
    {
        nDuration = aSlideTransitionHandler.getDuration().getValue();
    }
    else
    {
        log( 'SlideShow.createSlideTransition: duration is not a number' );
    }

    var aCommonParameterSet = new ActivityParamSet();
    aCommonParameterSet.aEndEvent = aTransitionEndEvent;
    aCommonParameterSet.aTimerEventQueue = this.aTimerEventQueue;
    aCommonParameterSet.aActivityQueue = this.aActivityQueue;
    aCommonParameterSet.nMinDuration = nDuration;
    aCommonParameterSet.nMinNumberOfFrames = aSlideTransitionHandler.getMinFrameCount();
    aCommonParameterSet.nSlideWidth = WIDTH;
    aCommonParameterSet.nSlideHeight = HEIGHT;

    return new SimpleActivity( aCommonParameterSet, aSlideTransition, FORWARD );

};

SlideShow.prototype.isEnabled = function()
{
    return this.bIsEnabled;
};

SlideShow.prototype.isRunning = function()
{
    return !this.bIsIdle;
};

SlideShow.prototype.isTransitionPlaying = function()
{
    return this.bIsTransitionRunning;
};

SlideShow.prototype.isMainEffectPlaying = function()
{
    return this.bIsNextEffectRunning;
};

SlideShow.prototype.isInteractiveEffectPlaying = function()
{
    return ( this.nTotalInteractivePlayingEffects > 0 );
};

SlideShow.prototype.isAnyEffectPlaying = function()
{
    return ( this.isMainEffectPlaying() || this.isInteractiveEffectPlaying() );
};

SlideShow.prototype.hasAnyEffectStarted = function()
{
    return ( this.aStartedEffectList.length > 0 );
};

SlideShow.prototype.notifyNextEffectStart = function()
{
    assert( !this.bIsNextEffectRunning,
            'SlideShow.notifyNextEffectStart: an effect is already started.' );
    this.bIsNextEffectRunning = true;
    this.aEventMultiplexer.registerNextEffectEndHandler( bind2( SlideShow.prototype.notifyNextEffectEnd, this ) );
    var aEffect = new Effect();
    aEffect.start();
    this.aStartedEffectIndexMap[ -1 ] = this.aStartedEffectList.length;
    this.aStartedEffectList.push( aEffect );

    var aAnimatedElementMap = theMetaDoc.aMetaSlideSet[nCurSlide].aSlideAnimationsHandler.aAnimatedElementMap;
    for( var sId in aAnimatedElementMap )
        aAnimatedElementMap[ sId ].notifyNextEffectStart( this.nCurrentEffect );
};

SlideShow.prototype.notifyNextEffectEnd = function()
{
    assert( this.bIsNextEffectRunning,
            'SlideShow.notifyNextEffectEnd: effect already ended.' );
    this.bIsNextEffectRunning = false;

    this.aStartedEffectList[ this.aStartedEffectIndexMap[ -1 ] ].end();
    if( this.automaticAdvanceTimeout !== null )
    {
        if( this.automaticAdvanceTimeout['rewindedEffect'] === this.nCurrentEffect )
        {
            this.automaticAdvanceTimeout = null;
            this.notifyAnimationsEnd();
        }
    }
};

SlideShow.prototype.notifyAnimationsEnd = function()
{
    if( nCurSlide + 1 === theMetaDoc.nNumberOfSlides )
        return;

    assert (this.automaticAdvanceTimeout === null,
        'SlideShow.notifyAnimationsEnd: Timeout already set.')

    var nTimeout = Math.ceil(theMetaDoc.aMetaSlideSet[nCurSlide].fDuration * 1000);
    if( nTimeout < 0 )
        return;

    this.automaticAdvanceTimeout = window.setTimeout('switchSlide(1, false)', nTimeout);
};

SlideShow.prototype.notifySlideStart = function( nNewSlideIndex, nOldSlideIndex )
{
    this.nCurrentEffect = 0;
    this.bIsRewinding = false;
    this.bIsSkipping = false;
    this.bIsSkippingAll = false;
    this.nTotalInteractivePlayingEffects = 0;
    this.aStartedEffectList = [];
    this.aStartedEffectIndexMap = {};
    this.aStartedEffectIndexMap[ -1 ] = undefined;

    var aAnimatedElementMap;
    var sId;
    if( nOldSlideIndex !== undefined )
    {
        aAnimatedElementMap = theMetaDoc.aMetaSlideSet[nOldSlideIndex].aSlideAnimationsHandler.aAnimatedElementMap;
        for( sId in aAnimatedElementMap )
            aAnimatedElementMap[ sId ].notifySlideEnd();
    }

    aAnimatedElementMap = theMetaDoc.aMetaSlideSet[nNewSlideIndex].aSlideAnimationsHandler.aAnimatedElementMap;
    for( sId in aAnimatedElementMap )
        aAnimatedElementMap[ sId ].notifySlideStart( this.aContext );
};

SlideShow.prototype.notifyTransitionEnd = function( nSlideIndex )
{
    // reset the presentation clip path on the leaving slide
    // to the standard one when transition ends
    if( theMetaDoc.getCurrentSlide() )
    {
        var sRef = 'url(#' + aPresentationClipPathId + ')';
        theMetaDoc.getCurrentSlide().slideElement.setAttribute('clip-path', sRef);
    }

    this.bIsTransitionRunning = false;
    if( this.bIsRewinding )
    {
        theMetaDoc.aMetaSlideSet[nSlideIndex].hide();
        var nIndex = nCurSlide !== undefined ? nCurSlide : -1;
        this.displaySlide( nIndex, true );
        this.skipAllEffects();
        this.bIsRewinding = false;
        return;
    }

    theMetaDoc.setCurrentSlide(nSlideIndex);

    if( this.aSlideViewElement )
    {
        theMetaDoc.getCurrentSlide().aVisibilityStatusElement.parentNode.removeChild( this.aSlideViewElement );
        this.aSlideViewElement = null;
    }
    if( this.isEnabled() )
    {
        // clear all queues
        this.dispose();

        var aCurrentSlide = theMetaDoc.getCurrentSlide();
        if( aCurrentSlide.aSlideAnimationsHandler.elementsParsed() )
        {
			aCurrentSlide.aSlideAnimationsHandler.start();
			this.aEventMultiplexer.registerAnimationsEndHandler( bind2( SlideShow.prototype.notifyAnimationsEnd, this ) );
        }
        else
            this.notifyAnimationsEnd();

        this.update();
    }
    else
        this.notifyAnimationsEnd();
};

SlideShow.prototype.notifyInteractiveAnimationSequenceStart = function( nNodeId )
{
    ++this.nTotalInteractivePlayingEffects;
    var aEffect = new Effect( nNodeId );
    aEffect.start();
    this.aStartedEffectIndexMap[ nNodeId ] = this.aStartedEffectList.length;
    this.aStartedEffectList.push( aEffect );
};

SlideShow.prototype.notifyInteractiveAnimationSequenceEnd = function( nNodeId )
{
    assert( this.isInteractiveEffectPlaying(),
            'SlideShow.notifyInteractiveAnimationSequenceEnd: no interactive effect playing.' );

    this.aStartedEffectList[ this.aStartedEffectIndexMap[ nNodeId ] ].end();
    --this.nTotalInteractivePlayingEffects;
};

/** nextEffect
 *  Start the next effect belonging to the main animation sequence if any.
 *  If there is an already playing effect belonging to any animation sequence
 *  it is skipped.
 *
 *  @return {Boolean}
 *      False if there is no more effect to start, true otherwise.
 */
SlideShow.prototype.nextEffect = function()
{
    if( !this.isEnabled() )
        return false;

    if( this.isTransitionPlaying() )
    {
        this.skipTransition();
        return true;
    }

    if( this.isAnyEffectPlaying() )
    {
        this.skipAllPlayingEffects();
        return true;
    }

    if( !this.aNextEffectEventArray )
        return false;

    if( this.nCurrentEffect >= this.aNextEffectEventArray.size() )
        return false;

    this.notifyNextEffectStart();

    this.aNextEffectEventArray.at( this.nCurrentEffect ).fire();
    ++this.nCurrentEffect;
    this.update();
    return true;
};

/** skipTransition
 *  Skip the current playing slide transition.
 */
SlideShow.prototype.skipTransition  = function()
{
    if( this.bIsSkipping || this.bIsRewinding )
        return;

    this.bIsSkipping = true;

    this.aActivityQueue.endAll();
    this.aTimerEventQueue.forceEmpty();
    this.aActivityQueue.endAll();
    this.update();
    this.bIsSkipping = false;
};

/** skipAllPlayingEffects
 *  Skip all playing effect, independently to which animation sequence they
 *  belong.
 *
 */
SlideShow.prototype.skipAllPlayingEffects  = function()
{
    if( this.bIsSkipping || this.bIsRewinding )
        return true;

    this.bIsSkipping = true;
    // TODO: The correct order should be based on the left playing time.
    for( var i = 0; i < this.aStartedEffectList.length; ++i )
    {
        var aEffect = this.aStartedEffectList[i];
        if( aEffect.isPlaying() )
        {
            if( aEffect.isMainEffect() )
                this.aEventMultiplexer.notifySkipEffectEvent();
            else
                this.aEventMultiplexer.notifySkipInteractiveEffectEvent( aEffect.getId() );
        }
    }
    this.update();
    this.bIsSkipping = false;
    return true;
};

/** skipNextEffect
 *  Skip the next effect to be played (if any) that belongs to the main
 *  animation sequence.
 *  Require: no effect is playing.
 *
 *  @return {Boolean}
 *      False if there is no more effect to skip, true otherwise.
 */
SlideShow.prototype.skipNextEffect = function()
{
    if( this.bIsSkipping || this.bIsRewinding )
        return true;

    assert( !this.isAnyEffectPlaying(),
            'SlideShow.skipNextEffect' );

    if( !this.aNextEffectEventArray )
        return false;

    if( this.nCurrentEffect >= this.aNextEffectEventArray.size() )
        return false;

    this.notifyNextEffectStart();

    this.bIsSkipping = true;
    this.aNextEffectEventArray.at( this.nCurrentEffect ).fire();
    this.aEventMultiplexer.notifySkipEffectEvent();
    ++this.nCurrentEffect;
    this.update();
    this.bIsSkipping = false;
    return true;
};

/** skipPlayingOrNextEffect
 *  Skip the next effect to be played that belongs to the main animation
 *  sequence  or all playing effects.
 *
 *  @return {Boolean}
 *      False if there is no more effect to skip, true otherwise.
 */
SlideShow.prototype.skipPlayingOrNextEffect = function()
{
    if( this.isTransitionPlaying() )
    {
        this.skipTransition();
        return true;
    }

    if( this.isAnyEffectPlaying() )
        return this.skipAllPlayingEffects();
    else
        return this.skipNextEffect();
};


/** skipAllEffects
 *  Skip all left effects that belongs to the main animation sequence and all
 *  playing effects on the current slide.
 *
 *  @return {Boolean}
 *      True if it already skipping or when it has ended skipping,
 *      false if the next slide needs to be displayed.
 */
SlideShow.prototype.skipAllEffects = function()
{
    if( this.bIsSkippingAll )
        return true;

    this.bIsSkippingAll = true;

    if( this.isTransitionPlaying() )
    {
        this.skipTransition();
    }

    if( this.isAnyEffectPlaying() )
    {
        this.skipAllPlayingEffects();
    }
    else if( !this.aNextEffectEventArray
               || ( this.nCurrentEffect >= this.aNextEffectEventArray.size() ) )
    {
        this.bIsSkippingAll = false;
        return false;
    }

    // Pay attention here: a new next effect event is appended to
    // aNextEffectEventArray only after the related animation node has been
    // resolved, that is only after the animation node related to the previous
    // effect has notified to be deactivated to the main sequence time container.
    // So you should avoid any optimization here because the size of
    // aNextEffectEventArray will going on increasing after every skip action.
    while( this.nCurrentEffect < this.aNextEffectEventArray.size() )
    {
        this.skipNextEffect();
    }
    this.bIsSkippingAll = false;
    return true;
};

/** rewindTransition
 * Rewind the current playing slide transition.
 */
SlideShow.prototype.rewindTransition = function()
{
    if( this.bIsSkipping || this.bIsRewinding )
    return;

    this.bIsRewinding = true;
    this.aActivityQueue.endAll();
    this.update();
    this.bIsRewinding = false;
};

/** rewindEffect
 *  Rewind all the effects started after at least one of the current playing
 *  effects. If there is no playing effect, it rewinds the last played one,
 *  both in case it belongs to the main or to an interactive animation sequence.
 *
 */
SlideShow.prototype.rewindEffect = function()
{
    if( this.bIsSkipping || this.bIsRewinding )
        return;

	if( this.automaticAdvanceTimeout !== null && !this.automaticAdvanceTimeout['rewindedEffect'] )
	{
		window.clearTimeout( this.automaticAdvanceTimeout );
		this.automaticAdvanceTimeout = { 'rewindedEffect': this.nCurrentEffect };
	}

    if( !this.hasAnyEffectStarted() )
    {
        this.rewindToPreviousSlide();
        return;
    }

    this.bIsRewinding = true;

    var nFirstPlayingEffectIndex = undefined;

    var i = 0;
    for( ; i < this.aStartedEffectList.length; ++i )
    {
        var aEffect = this.aStartedEffectList[i];
        if( aEffect.isPlaying() )
        {
            nFirstPlayingEffectIndex = i;
            break;
        }
    }

    // There is at least one playing effect.
    if( nFirstPlayingEffectIndex !== undefined )
    {
        i = this.aStartedEffectList.length - 1;
        for( ; i >= nFirstPlayingEffectIndex; --i )
        {
            aEffect = this.aStartedEffectList[i];
            if( aEffect.isPlaying() )
            {
                if( aEffect.isMainEffect() )
                {
                    this.aEventMultiplexer.notifyRewindCurrentEffectEvent();
                    if( this.nCurrentEffect > 0 )
                        --this.nCurrentEffect;
                }
                else
                {
                    this.aEventMultiplexer.notifyRewindRunningInteractiveEffectEvent( aEffect.getId() );
                }
            }
            else if( aEffect.isEnded() )
            {
                if( aEffect.isMainEffect() )
                {
                    this.aEventMultiplexer.notifyRewindLastEffectEvent();
                    if( this.nCurrentEffect > 0 )
                        --this.nCurrentEffect;
                }
                else
                {
                    this.aEventMultiplexer.notifyRewindEndedInteractiveEffectEvent( aEffect.getId() );
                }
            }
        }
        this.update();

        // Pay attention here: we need to remove all rewinded effects from
        // the started effect list only after updating.
        i = this.aStartedEffectList.length - 1;
        for( ; i >= nFirstPlayingEffectIndex; --i )
        {
            aEffect = this.aStartedEffectList.pop();
            if( !aEffect.isMainEffect() )
                delete this.aStartedEffectIndexMap[ aEffect.getId() ];
        }
    }
    else  // there is no playing effect
    {
        aEffect = this.aStartedEffectList.pop();
        if( !aEffect.isMainEffect() )
            delete this.aStartedEffectIndexMap[ aEffect.getId() ];
        if( aEffect.isEnded() )  // Well that is almost an assertion.
        {
            if( aEffect.isMainEffect() )
            {
                this.aEventMultiplexer.notifyRewindLastEffectEvent();
                if( this.nCurrentEffect > 0 )
                    --this.nCurrentEffect;
            }
            else
            {
                this.aEventMultiplexer.notifyRewindEndedInteractiveEffectEvent( aEffect.getId() );
            }
        }
        this.update();
    }

    this.bIsRewinding = false;
};

/** rewindToPreviousSlide
 *  Displays the previous slide with all effects, that belong to the main
 *  animation sequence, played.
 *
 */
SlideShow.prototype.rewindToPreviousSlide = function()
{
    if( this.isTransitionPlaying() )
    {
        this.rewindTransition();
        return;
    }
    if( this.isAnyEffectPlaying() )
        return;
    var nNewSlide = nCurSlide - 1;
    this.displaySlide( nNewSlide, true );
    this.skipAllEffects();
};

/** rewindAllEffects
 *  Rewind all effects already played on the current slide.
 *
 */
SlideShow.prototype.rewindAllEffects = function()
{
    if( !this.hasAnyEffectStarted() )
    {
        this.rewindToPreviousSlide();
        return;
    }

    while( this.hasAnyEffectStarted() )
    {
        this.rewindEffect();
    }
};

SlideShow.prototype.exitSlideShowInApp = function()
{
    if (window.webkit !== undefined &&
        window.webkit.messageHandlers !== undefined &&
        window.webkit.messageHandlers.lok !== undefined)
        window.webkit.messageHandlers.lok.postMessage('EXITSLIDESHOW', '*');
}

SlideShow.prototype.displaySlide = function( nNewSlide, bSkipSlideTransition )
{
    var aMetaDoc = theMetaDoc;
    var nSlides = aMetaDoc.nNumberOfSlides;
    if( nNewSlide < 0 && nSlides > 0 )
        nNewSlide = nSlides - 1;
    else if( nNewSlide >= nSlides ) {
        nNewSlide = 0;
        // In the iOS app, exit the slideshow when going past the end.
        this.exitSlideShowInApp();
    }

    if( ( currentMode === INDEX_MODE ) && ( nNewSlide === nCurSlide ) )
    {
        aMetaDoc.getCurrentSlide().show();
        return;
    }

    if( this.isTransitionPlaying() )
    {
        this.skipTransition();
    }

    // handle current slide
    var nOldSlide = nCurSlide;
    if( nOldSlide !== undefined )
    {
        var oldMetaSlide = aMetaDoc.aMetaSlideSet[nOldSlide];
        if( this.isEnabled() )
        {
            if( oldMetaSlide.aSlideAnimationsHandler.isAnimated() )
            {
                // force end animations
                oldMetaSlide.aSlideAnimationsHandler.end( bSkipSlideTransition );

                // clear all queues
                this.dispose();
            }
        }

        if( this.automaticAdvanceTimeout !== null )
        {
            window.clearTimeout( this.automaticAdvanceTimeout );
            this.automaticAdvanceTimeout = null;
        }
    }

    this.notifySlideStart( nNewSlide, nOldSlide );

    if( this.isEnabled() && !bSkipSlideTransition  )
    {
        // create slide transition and add to activity queue
        if ( ( ( nOldSlide !== undefined ) &&
               ( ( nNewSlide > nOldSlide ) ||
               ( ( nNewSlide == 0) && ( nOldSlide == (aMetaDoc.nNumberOfSlides - 1) ) ) ) ) ||
             (  ( nOldSlide === undefined ) &&  ( nNewSlide == 0) )  // for transition on first slide
           )
        {

            var aOldMetaSlide = null;
            if( nOldSlide === undefined ) // for transition on first slide
            {
                aOldMetaSlide = aMetaDoc.theMetaDummySlide;
            }
            else
            {
                aOldMetaSlide = aMetaDoc.aMetaSlideSet[nOldSlide];
            }
            var aNewMetaSlide = aMetaDoc.aMetaSlideSet[nNewSlide];

            var aSlideTransitionHandler = aNewMetaSlide.aTransitionHandler;
            if( aSlideTransitionHandler && aSlideTransitionHandler.isValid() )
            {
                // clipPath element used for the leaving slide in order
                // to avoid that slide borders are visible during transition
                var sRef = 'url(#' + aPresentationClipPathShrinkId + ')';
                aOldMetaSlide.slideElement.setAttribute( 'clip-path', sRef );

                // when we switch from the last to the first slide we need to hide the last slide
                // or nobody will see the transition, hence we create a view of the last slide and
                // we place it before the first slide
                if( nOldSlide > nNewSlide )
                {
                    this.aSlideViewElement = document.createElementNS( NSS['svg'], 'use' );
                    setNSAttribute( 'xlink', this.aSlideViewElement, 'href', '#' + aOldMetaSlide.slideContainerId );
                    aNewMetaSlide.aVisibilityStatusElement.parentNode.insertBefore( this.aSlideViewElement, aNewMetaSlide.aVisibilityStatusElement );
                    aOldMetaSlide.hide();
                }

                var aLeavingSlide = aOldMetaSlide;
                var aEnteringSlide = aNewMetaSlide;
                var aTransitionEndEvent = makeEvent( bind2( this.notifyTransitionEnd, this, nNewSlide ) );

                var aTransitionActivity =
                    this.createSlideTransition( aSlideTransitionHandler, aLeavingSlide,
                                                aEnteringSlide, aTransitionEndEvent );

                if( aTransitionActivity )
                {
                    this.bIsTransitionRunning = true;
                    this.aActivityQueue.addActivity( aTransitionActivity );
                    this.update();
                }
                else
                {
                    this.notifyTransitionEnd( nNewSlide );
                }
            }
            else
            {
                this.notifyTransitionEnd( nNewSlide );
            }
        }
        else
        {
            this.notifyTransitionEnd( nNewSlide );
        }
    }
    else
    {
        this.notifyTransitionEnd( nNewSlide );
    }

};

SlideShow.prototype.update = function()
{
    this.aTimer.holdTimer();

    // process queues
    this.aTimerEventQueue.process();
    this.aActivityQueue.process();

    this.aFrameSynchronization.synchronize();

    this.aActivityQueue.processDequeued();

    this.aTimer.releaseTimer();

    var bActivitiesLeft = ( ! this.aActivityQueue.isEmpty() );
    var bTimerEventsLeft = ( ! this.aTimerEventQueue.isEmpty() );
    var bEventsLeft = ( bActivitiesLeft || bTimerEventsLeft );


    if( bEventsLeft )
    {
        var nNextTimeout;
        if( bActivitiesLeft )
        {
            nNextTimeout = MINIMUM_TIMEOUT;
            this.aFrameSynchronization.activate();
        }
        else
        {
            nNextTimeout = this.aTimerEventQueue.nextTimeout();
            if( nNextTimeout < MINIMUM_TIMEOUT )
                nNextTimeout = MINIMUM_TIMEOUT;
            else if( nNextTimeout > MAXIMUM_TIMEOUT )
                nNextTimeout = MAXIMUM_TIMEOUT;
            this.aFrameSynchronization.deactivate();
        }

        this.bIsIdle = false;
        window.setTimeout( 'aSlideShow.update()', nNextTimeout * 1000 );
    }
    else
    {
        this.bIsIdle = true;
    }
};

SlideShow.prototype.dispose = function()
{
    // clear all queues
    this.aTimerEventQueue.clear();
    this.aActivityQueue.clear();
    this.aNextEffectEventArray = null;
    this.aEventMultiplexer = null;
};

SlideShow.prototype.getContext = function()
{
    return this.aContext;
};

// the SlideShow global instance
var aSlideShow = null;




function SlideShowContext( aTimerEventQueue, aEventMultiplexer, aNextEffectEventArray, aInteractiveAnimationSequenceMap, aActivityQueue)
{
    this.aTimerEventQueue = aTimerEventQueue;
    this.aEventMultiplexer = aEventMultiplexer;
    this.aNextEffectEventArray = aNextEffectEventArray;
    this.aInteractiveAnimationSequenceMap = aInteractiveAnimationSequenceMap;
    this.aActivityQueue = aActivityQueue;
    this.bIsSkipping = false;
}




function FrameSynchronization( nFrameDuration )
{
    this.nFrameDuration = nFrameDuration;
    this.aTimer = new ElapsedTime();
    this.nNextFrameTargetTime = 0.0;
    this.bIsActive = false;

    this.markCurrentFrame();
}


FrameSynchronization.prototype.markCurrentFrame = function()
{
    this.nNextFrameTargetTime = this.aTimer.getElapsedTime() + this.nFrameDuration;
};

FrameSynchronization.prototype.synchronize = function()
{
    if( this.bIsActive )
    {
        // Do busy waiting for now.
        while( this.aTimer.getElapsedTime() < this.nNextFrameTargetTime )
            ;
    }

    this.markCurrentFrame();

};

FrameSynchronization.prototype.activate = function()
{
    this.bIsActive = true;
};

FrameSynchronization.prototype.deactivate = function()
{
    this.bIsActive = false;
};



/**********************************************************************************************
 *      TimerEventQueue, ActivityQueue and ElapsedTime
 **********************************************************************************************/


function NextEffectEventArray()
{
    this.aEventArray = [];
}


NextEffectEventArray.prototype.size = function()
{
    return this.aEventArray.length;
};

NextEffectEventArray.prototype.at = function( nIndex )
{
    return this.aEventArray[ nIndex ];
};

NextEffectEventArray.prototype.appendEvent = function( aEvent )
{
    var nSize = this.size();
    for( var i = 0; i < nSize; ++i )
    {
        if( this.aEventArray[i].getId() == aEvent.getId() )
        {
            aNextEffectEventArrayDebugPrinter.print( 'NextEffectEventArray.appendEvent: event(' + aEvent.getId() + ') already present' );
            return false;
        }
    }
    this.aEventArray.push( aEvent );
    aNextEffectEventArrayDebugPrinter.print( 'NextEffectEventArray.appendEvent: event(' + aEvent.getId() + ') appended' );
    return true;
};

NextEffectEventArray.prototype.clear = function( )
{
    this.aEventArray = [];
};




function TimerEventQueue( aTimer )
{
    this.aTimer = aTimer;
    this.aEventSet = new PriorityQueue( EventEntry.compare );
}


TimerEventQueue.prototype.addEvent = function( aEvent )
{
    this.DBG( 'TimerEventQueue.addEvent event(' + aEvent.getId() + ') appended.' );
    if( !aEvent )
    {
        log( 'TimerEventQueue.addEvent: null event' );
        return false;
    }

    var nTime = aEvent.getActivationTime( this.aTimer.getElapsedTime() );
    var aEventEntry = new EventEntry( aEvent, nTime );
    this.aEventSet.push( aEventEntry );

    return true;
};

TimerEventQueue.prototype.forceEmpty = function()
{
    this.process_(true);
};


TimerEventQueue.prototype.process = function()
{
    this.process_(false);
};

TimerEventQueue.prototype.process_ = function( bFireAllEvents )
{
    var nCurrentTime = this.aTimer.getElapsedTime();

    while( !this.isEmpty() && ( bFireAllEvents || ( this.aEventSet.top().nActivationTime <= nCurrentTime ) ) )
    {
        var aEventEntry = this.aEventSet.top();
        this.aEventSet.pop();

        var aEvent = aEventEntry.aEvent;
        if( aEvent.isCharged() )
            aEvent.fire();
    }
};

TimerEventQueue.prototype.isEmpty = function()
{
    return this.aEventSet.isEmpty();
};

TimerEventQueue.prototype.nextTimeout = function()
{
    var nTimeout = Number.MAX_VALUE;
    var nCurrentTime = this.aTimer.getElapsedTime();
    if( !this.isEmpty() )
        nTimeout = this.aEventSet.top().nActivationTime - nCurrentTime;
    return nTimeout;
};

TimerEventQueue.prototype.clear = function()
{
    this.DBG( 'TimerEventQueue.clear invoked' );
    this.aEventSet.clear();
};

TimerEventQueue.prototype.getTimer = function()
{
    return this.aTimer;
};

TimerEventQueue.prototype.DBG = function( sMessage, nTime )
{
    aTimerEventQueueDebugPrinter.print( sMessage, nTime );
};


TimerEventQueue.prototype.insert = function( aEventEntry )
{
    var nHoleIndex = this.aEventSet.length;
    var nParent = Math.floor( ( nHoleIndex - 1 ) / 2 );

    while( ( nHoleIndex > 0 ) && this.aEventSet[ nParent ].compare( aEventEntry ) )
    {
        this.aEventSet[ nHoleIndex ] = this.aEventSet[ nParent ];
        nHoleIndex = nParent;
        nParent = Math.floor( ( nHoleIndex - 1 ) / 2 );
    }
    this.aEventSet[ nHoleIndex ] = aEventEntry;
};




function EventEntry( aEvent, nTime )
{
    this.aEvent = aEvent;
    this.nActivationTime = nTime;
}


EventEntry.compare = function( aLhsEventEntry, aRhsEventEntry )
{
    if ( aLhsEventEntry.nActivationTime > aRhsEventEntry.nActivationTime )
    {
        return -1;
    }
    else if ( aLhsEventEntry.nActivationTime < aRhsEventEntry.nActivationTime )
    {
        return 1;
    }
    else
    {
        return 0;
    }
};




function ActivityQueue( aTimer )
{
    this.aTimer = aTimer;
    this.aCurrentActivityWaitingSet = [];
    this.aCurrentActivityReinsertSet = [];
    this.aDequeuedActivitySet = [];
}


ActivityQueue.prototype.dispose = function()
{
    var nSize = this.aCurrentActivityWaitingSet.length;
    var i;
    for( i = 0; i < nSize; ++i )
        this.aCurrentActivityWaitingSet[i].dispose();

    nSize = this.aCurrentActivityReinsertSet.length;
    for( i = 0; i < nSize; ++i )
        this.aCurrentActivityReinsertSet[i].dispose();
};

ActivityQueue.prototype.addActivity = function( aActivity )
{
    if( !aActivity )
    {
        log( 'ActivityQueue.addActivity: activity is not valid' );
        return false;
    }

    this.aCurrentActivityWaitingSet.push( aActivity );
    aActivityQueueDebugPrinter.print( 'ActivityQueue.addActivity: activity appended' );
    return true;
};

ActivityQueue.prototype.process = function()
{
    var nSize = this.aCurrentActivityWaitingSet.length;
    var nLag = 0.0;
    for( var i = 0; i < nSize; ++i )
    {
        nLag = Math.max( nLag,this.aCurrentActivityWaitingSet[i].calcTimeLag()  );
    }

    if( nLag > 0.0 )
        this.aTimer.adjustTimer( -nLag, true );


    while( this.aCurrentActivityWaitingSet.length != 0 )
    {
        var aActivity = this.aCurrentActivityWaitingSet.shift();
        var bReinsert = false;

        bReinsert = aActivity.perform();

        if( bReinsert )
        {
            this.aCurrentActivityReinsertSet.push( aActivity );
        }
        else
        {
            this.aDequeuedActivitySet.push( aActivity );
        }
    }

    if( this.aCurrentActivityReinsertSet.length != 0 )
    {
        // TODO: optimization, try to swap reference here
        this.aCurrentActivityWaitingSet = this.aCurrentActivityReinsertSet;
        this.aCurrentActivityReinsertSet = [];
    }
};

ActivityQueue.prototype.processDequeued = function()
{
    // notify all dequeued activities from last round
    var nSize = this.aDequeuedActivitySet.length;
    for( var i = 0; i < nSize; ++i )
        this.aDequeuedActivitySet[i].dequeued();

    this.aDequeuedActivitySet = [];
};

ActivityQueue.prototype.isEmpty = function()
{
    return ( ( this.aCurrentActivityWaitingSet.length == 0 ) &&
             ( this.aCurrentActivityReinsertSet.length == 0 ) );
};

ActivityQueue.prototype.clear = function()
{
    aActivityQueueDebugPrinter.print( 'ActivityQueue.clear invoked' );
    var nSize = this.aCurrentActivityWaitingSet.length;
    var i;
    for( i = 0; i < nSize; ++i )
        this.aCurrentActivityWaitingSet[i].dequeued();
    this.aCurrentActivityWaitingSet = [];

    nSize = this.aCurrentActivityReinsertSet.length;
    for( i = 0; i < nSize; ++i )
        this.aCurrentActivityReinsertSet[i].dequeued();
    this.aCurrentActivityReinsertSet = [];
};

ActivityQueue.prototype.endAll = function()
{
    aActivityQueueDebugPrinter.print( 'ActivityQueue.endAll invoked' );
    var nSize = this.aCurrentActivityWaitingSet.length;
    var i;
    for( i = 0; i < nSize; ++i )
        this.aCurrentActivityWaitingSet[i].end();
    this.aCurrentActivityWaitingSet = [];

    nSize = this.aCurrentActivityReinsertSet.length;
    for( i = 0; i < nSize; ++i )
        this.aCurrentActivityReinsertSet[i].end();
    this.aCurrentActivityReinsertSet = [];
};

ActivityQueue.prototype.getTimer = function()
{
    return this.aTimer;
};

ActivityQueue.prototype.size = function()
{
    return ( this.aCurrentActivityWaitingSet.length +
             this.aCurrentActivityReinsertSet.length +
             this.aDequeuedActivitySet.length );
};




function ElapsedTime( aTimeBase )
{
    this.aTimeBase = aTimeBase;
    this.nLastQueriedTime = 0.0;
    this.nStartTime = this.getCurrentTime();
    this.nFrozenTime = 0.0;
    this.bInPauseMode = false;
    this.bInHoldMode = false;
}


ElapsedTime.prototype.getTimeBase = function()
{
    return this.aTimeBase;
};

ElapsedTime.prototype.reset = function()
{
    this.nLastQueriedTime = 0.0;
    this.nStartTime = this.getCurrentTime();
    this.nFrozenTime = 0.0;
    this.bInPauseMode = false;
    this.bInHoldMode = false;
};

ElapsedTime.prototype.getElapsedTime = function()
{
    this.nLastQueriedTime = this.getElapsedTimeImpl();
    return this.nLastQueriedTime;
};

ElapsedTime.prototype.pauseTimer = function()
{
    this.nFrozenTime = this.getElapsedTimeImpl();
    this.bInPauseMode = true;
};

ElapsedTime.prototype.continueTimer = function()
{
    this.bInPauseMode = false;

    // stop pausing, time runs again. Note that
    // getElapsedTimeImpl() honors hold mode, i.e. a
    // continueTimer() in hold mode will preserve the latter
    var nPauseDuration = this.getElapsedTimeImpl() - this.nFrozenTime;

    // adjust start time, such that subsequent getElapsedTime() calls
    // will virtually start from m_fFrozenTime.
    this.nStartTime += nPauseDuration;
};

ElapsedTime.prototype.adjustTimer = function( nOffset, bLimitToLastQueriedTime )
{
    if( bLimitToLastQueriedTime == undefined )
        bLimitToLastQueriedTime = true;

    // to make getElapsedTime() become _larger_, have to reduce nStartTime.
    this.nStartTime -= nOffset;

    // also adjust frozen time, this method must _always_ affect the
    // value returned by getElapsedTime()!
    if( this.bInHoldMode || this.bInPauseMode )
        this.nFrozenTime += nOffset;
};

ElapsedTime.prototype.holdTimer = function()
{
    // when called during hold mode (e.g. more than once per time
    // object), the original hold time will be maintained.
    this.nFrozenTime = this.getElapsedTimeImpl();
    this.bInHoldMode = true;
};

ElapsedTime.prototype.releaseTimer = function()
{
    this.bInHoldMode = false;
};

ElapsedTime.prototype.getSystemTime = function()
{
    return ( getCurrentSystemTime() / 1000.0 );
};

ElapsedTime.prototype.getCurrentTime = function()
{
    var nCurrentTime;
    if ( !this.aTimeBase )
    {
        nCurrentTime = this.getSystemTime();
    }
    else
    {
        nCurrentTime = this.aTimeBase.getElapsedTimeImpl();
    }

    assert( ( typeof( nCurrentTime ) === typeof( 0 ) ) && isFinite( nCurrentTime ),
            'ElapsedTime.getCurrentTime: assertion failed: nCurrentTime == ' + nCurrentTime );


    return nCurrentTime;
};

ElapsedTime.prototype.getElapsedTimeImpl = function()
{
    if( this.bInHoldMode || this.bInPauseMode )
    {
        return this.nFrozenTime;
    }

    var nCurTime = this.getCurrentTime();
    return ( nCurTime - this.nStartTime );
};



/*****
 * @libreofficeend
 *
 * Several parts of the above code are the result of the porting,
 * started on August 2011, of the C++ code included in the source files
 * placed under the folder '/slideshow/source' and subfolders.
 * @source https://cgit.freedesktop.org/libreoffice/core/tree/slideshow/source
 *
 */

/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
