/* CONCAT of
/2static/script/fecru/data-structures.js
/2static/script/fecru/decorators.js
/2static/script/fecru/mixins.js
/2static/script/fecru/modernise.js
/2static/script/fecru/ajax.js
/2static/script/fecru/browse.js
/2static/script/fecru/dialog.js
/2static/script/fecru/gwtutil.js
/2static/script/fecru/hover.js
/2static/script/fecru/profile.js
/2static/script/fecru/rss.js
/2static/script/fecru/ui.js
/2static/script/fecru/util.js
/2static/script/fecru/recently-visited.js
/2static/script/fecru/onReady.js
/2static/script/fecru/search.js
/2static/script/fecru/star.js
/2static/script/fecru/applinks.js
/2static/script/fecru/oauth.js
/2static/script/fecru/prefs.js
/2static/script/fecru/managed.js
/2static/script/lib/graphael/g.raphael.js
/2static/script/lib/graphael/g.bar.js
/2static/script/lib/graphael/g.pie.js
/2static/script/lib/graphael/g.line.js
/2static/script/lib/graphael/g.dot.js
/2static/script/fecru/raphaelCharts.js
/2static/script/fecru/navbuilder.js
/script/common/global.js
/2static/script/lib/ajs/contentnamesearch.js
/2static/script/fe/fisheye-ui.js
/2static/script/fe/fisheye-article.js
/2static/script/fe/fisheye-activity.js
/2static/script/fe/fisheye-changeset.js
/2static/script/fe/fisheye-history.js
/2static/script/lib/json2.min.js
/2static/script/cru/util.js
/2static/script/cru/dialog/dialog-event.js
/2static/script/cru/review/email-comments.js
/2static/script/cru/review/review-history.js
*/
/* START /2static/script/fecru/data-structures.js */

//built-in changes
(function() {
    // Array Remove - By John Resig (MIT Licensed)
    Array.remove = function(array, from, to) {
        var rest = array.slice((to || from) + 1 || array.length);
        array.length = from < 0 ? array.length + from : from;
        return array.push.apply(array, rest);
    };

    Array.splice = function(array, index, count/*, replacement items... */) {
      var rest = array.slice((to || from) + 1 || array.length);
      array.length = from < 0 ? array.length + from : from;
      array.push.apply(array, arguments.slice(3));
      return array.push.apply(array, rest);
    };

    /*
     * Traverse an array, calling func on each array value in indexed order.
     *
     * @param array  array to traverse
     * @param func   function taking an array value as its first parameter and an array index as its second.
     */
    Array.each = function(array, func) {
        for (var i = 0, l = array.length; i < l && func(array[i], i) !== false; i++) {}
    };

    /*
     * Given an array, and a function that returns true/false, Array.any returns
     * true if there is at least one value in the array for which the function returns true.
     *
     * @param array      array to traverse
     * @param predicate  function taking an array value as its first parameter and an array index as its second.
     * @return           true if predicate returns true for any array value, false otherwise.
     */
    Array.any = function(array, predicate) {
        var i = 0,
            l = array.length;
        for (; i < l && !predicate(array[i], i); i++) {}
        return i !== l; // return false if empty array
    };
    /*
     * Given an array, and a function that returns true/false, Array.all returns
     * true if the function returns true for all array values (or if the array is empty).
     *
     * @param array      array to traverse
     * @param predicate  function taking an array value as its first parameter and an array index as its second.
     * @return           true if array is empty or predicate returns true for all array values, false otherwise.
     */
    Array.all = function(array, predicate) {
        var i = 0,
            l = array.length;
        for (; i < l && predicate(array[i], i); i++) {}
        return i === l; // return true if empty array
    };
    /*
     * @return the first value in the array for which predicate is true, or undefined if predicate never returns true.
     */
    Array.first = function(array, predicate) {
        var i = 0,
            l = array.length;
        for (; i < l && !predicate(array[i], i); i++) {}
        return i !== l ? array[i] : undefined;
    };
    /*
     * @return the last value in the array for which predicate is true, or undefined if predicate never returns true.
     */
    Array.last = function(array, predicate) {
        var i = array.length - 1;
        for (; i >= 0 && !predicate(array[i], i); i--) {}
        return i >= 0 ? array[i] : undefined;
    };

    /*
     * Returns a new array containing the results of func applied to each value in the given array.
     * E.g. if output = Array.map(input, func),
     *    then output[i] = func(input[i]), where i is any index in the input array.
     *
     * @param array  An array to traverse
     * @param func   A func to apply to each array value and whose return value will be used in the resulting array
     * @return       An array containing the result of func applied to each array value.
     */
    Array.map = function(array, func) {
        var result = [];
        Array.each(array, function (element) {
            result.push(func(element));
        });
        return result;
    };
    /*
     * Combines all array values by calling combine on a resultSoFar and the next element. E.g.,
     * Array.reduce([1,2,3], 0, function(a,b) { return a + b; }) === 6,
     * Array.reduce([2,3,4], 1, function(a,b) { return a * b; }) === 24
     *
     * @param array    An array to reduce
     * @param base     A starting value
     * @param combine  A function taking two values and returning a new value.
     * @return         A single value resulting from cumulatively calling combine on the result of the last
     *                 combine call(or base) and the next array element.
     */
    Array.reduce = function (array, base, combine) {
        Array.each(array, function (element) {
            base = combine(base, element);
        });
        return base;
    };
})();

window.FECRU = window.FECRU || {};
FECRU.DATA_STRUCTURES = FECRU.DATA_STRUCTURES || {};
(function(namespace, $, window){

    function indexOf(array, item) {
        for (var i = 0, l = array.length; i < l; i++) {
            if (item === array[i].item) {
                return i;
            }
        }
        return -1;
    }

    function compare(a, b) {
        return a.comparable < b.comparable ? -1 :
               a.comparable > b.comparable ? 1 :
               0;
    }

    var PriorityQueue = function() {
        var m_queue = [],
            dirty = false;
        this.save = function(item, comparableValue) {
            var index = indexOf(m_queue, item);
            if (index !== -1) {
                dirty = true;
                m_queue[index].item = item;
                m_queue[index].comparable = comparableValue;
            } else {
                // set dirty if it was already dirty, or if this one doesn't belong at the end.
                dirty = dirty || (m_queue.length && compare(comparableValue, m_queue[m_queue.length - 1].comparable) < 0);
                m_queue.push({
                    item : item,
                    comparable : comparableValue
                });
            }
            return this;
        };
        this.remove = function() {
            if (dirty) {
                m_queue.sort(compare);
                dirty = false;
            }
            return m_queue.shift().item;
        };
        this.length = function() {
            return m_queue.length;
        };
    };
    var LRUQueue = function() {
        PriorityQueue.call(this);
        var superSave = this.save;
        this.save = function(item, optionalDate) {
            superSave.call(this,
                item, (optionalDate && optionalDate.getTime) ? optionalDate : new Date());
        };
    };

    //Object.prototype.hasOwnProperty can still be overwritten, but this is about as safe as we can get.
    var hasOwnProperty = window.Object.prototype.hasOwnProperty;
    /*
    Map is a safer way to create a hash map.
    Using an object directly won't support keys named 'hasOwnProperty' without breaking the very method
    you use to determine if the object has the key!

    ALL KEYS ARE CONVERTED TO STRING.  If using Map with non-String keys, ensure your key's toString function
    will create unique string representations.
    * */
    var Map = function() {
        var holder = {};
        this.has = function(key) {
            //holder.hasOwnProperty(key) would fail if someone had called .set('hasOwnProperty', 'break it')
            return hasOwnProperty.call(holder, key);
        };
        this.get = function(key) {
            return hasOwnProperty.call(holder, key) ? holder[key] : undefined;
        };
        this.set = function(key, value) {
            holder[key] = value;
        };
        this.remove = function(key) {
            return delete holder[key];
        };
        this.getKeys = function() {
            var arr = [];
            for(var key in holder) {
                if (hasOwnProperty.call(holder, key)) {
                    arr.push(key);
                }
            }
            return arr;
        };
        this.getValues = function() {
            var arr = [];
            for(var key in holder) {
                if (hasOwnProperty.call(holder, key)) {
                    arr.push(holder[key]);
                }
            }
            return arr;
        };
        this.clear = function() {
            holder = {};
        };
    };

    var CacheMap = function(entriesToStore) {
        Map.call(this);

        // if they pass in an invalid value, act like a normal map.
        if (entriesToStore <= 0) {
            return;
        }
        
        var deletionQueue = new LRUQueue();

        var oldSet = this.set,
            oldGet = this.get,
            oldRemove = this.remove;

        this.get = function(key) {
            deletionQueue.save(key);
            return oldGet.call(this, key);
        };

        this.set = function(key, value) {
            deletionQueue.save(key);
            oldSet.call(this, key, value);
            while (deletionQueue.length() > entriesToStore) {
                oldRemove.call(this, deletionQueue.remove());
            }
        };

        var sendToFront = new Date(0);
        this.remove = function(key) {
            oldRemove.call(this, key);
            deletionQueue.save(key, sendToFront);
        }
    };

    $.extend(namespace, {
        LeastRecentlyUsedQueue :  LRUQueue,
        Map : Map,
        CacheMap : CacheMap
    });
})(FECRU.DATA_STRUCTURES, AJS.$, this);

;
/* END /2static/script/fecru/data-structures.js */
/* START /2static/script/fecru/decorators.js */
FECRU.DECORATORS = FECRU.DECORATORS || {};
(function(namespace, $) {

    var memoize = function(prototype, funcKey) {
        var func = prototype[funcKey];

        var newFunc = function() {
            var self = this,
                val = func.call(self),
                isArray = val instanceof Array;
            var russianDoll = function() {
                return isArray ? val.slice(0) : val;
            };
            russianDoll.flush = function () {
                self[funcKey] = newFunc;
            };

            self[funcKey] = russianDoll;

            return isArray ? val.slice(0) : val;
        };
        newFunc.flush = function() {};

        prototype[funcKey] = newFunc;
    };

    var eraseMemoized = function(prototype, funcKey, flushFuncOrKeysToFlush) {
        var func = prototype[funcKey];
        if ($.isFunction(flushFuncOrKeysToFlush)) {
            var flushFunc = flushFuncOrKeysToFlush;
            prototype[funcKey] = function() {
                var ret = func.apply(this, arguments);
                flushFunc.apply(this, arguments);
                return ret;
            };
        } else if ($.isArray(flushFuncOrKeysToFlush)) {
            var funcKeysArray = flushFuncOrKeysToFlush;
            prototype[funcKey] = function() {
                var ret = func.apply(this, arguments),
                    self = this;
                Array.each(funcKeysArray, function(funcNameToFlush) {
                    self[funcNameToFlush].flush();
                });
                return ret;
            };
        } else {
            throw "Expected function or Array as third parameter.";
        }
    };

    $.extend(namespace, {
        memoize : memoize,
        eraseMemoized : eraseMemoized
    });

})(FECRU.DECORATORS, AJS.$);;
/* END /2static/script/fecru/decorators.js */
/* START /2static/script/fecru/mixins.js */
FECRU.MIXINS = FECRU.MIXINS || {};
(function(namespace, $) {

    /*
     * Usage: $.extend(MyClass.prototype, EventProducer);
     * Will add bind, unbind, trigger, and getBinder methods to objects of MyClass.
     */
    var EventProducer = (function() {
        /*
         * @param type - a string defining the event type to bind to.
         * @param handler - a function that will be called when the event is triggered.
         */
        function bind(type, handler) {
            var handlers = this._eventHandlers || (this._eventHandlers = {});
            var typeHandlers = handlers[type] || (handlers[type] = []);
            typeHandlers.push(handler);

            return this;
        }
        /*
         * @param type - a string defining the event type to unbind from.
         * @param handlerToRemove - the handler to unbind.
         */
        function unbind(type, handlerToRemove) {
            var handlers,
                typeHandlers,
                index;
            if ((handlers = this._eventHandlers) &&
                (typeHandlers = handlers[type]) &&
                (index = $.inArray(handlerToRemove, typeHandlers)) !== -1) {
                Array.remove(typeHandlers, index);
            }

            return this;
        }
        /*
         * @param type - a string defining the event type to trigger.
         * @param eventObj - an object with event-related properties.  A clone of the object will be passed to the handlers.
         */
        function trigger(type, eventObj, options) {
            if (eventObj != null && !$.isPlainObject(eventObj)) {
                throw "You must trigger an event with an object";
            }

            var shouldSetTimeout = options && options.clearCallStack,
                handlerRunner = shouldSetTimeout ? function(handler) {
                    setTimeout(function() {
                        handler($.extend({ type : type }, eventObj));
                    }, 0);
                } : function(handler) {
                    handler($.extend({ type : type }, eventObj));
                };

            var handlers,
                typeHandlers;
            if ((handlers = this._eventHandlers) &&
                (typeHandlers = handlers[type])) {
                var immutableHandlerList = typeHandlers.slice(0);
                Array.each(immutableHandlerList, handlerRunner);
            }
        }

        function makeBinder(target, binder) {
            binder.bind = function() {
                bind.apply(target, arguments);
            };
            binder.unbind = function() {
                unbind.apply(target, arguments);
            };

            if (target._eventNames) {
                Array.each(target._eventNames, function(eventName) {
                    binder[eventName] = function(func) {
                        bind.call(target, eventName, func);
                    };
                });
            }

            return binder;
        }

        /**
         * Returns an object with only a bind method.  When called, events are bound to the EventProducer instead.
         */
        function getBinder() {
            return makeBinder(this, {});
        }

        function EventProducer(eventNamesArray) {
            if (!this instanceof EventProducer) {
                return new EventProducer(eventNamesArray);
            }

            if (eventNamesArray && eventNamesArray.length) {
                this._eventNames = eventNamesArray;

                var self = this;
                Array.each(eventNamesArray, function(eventName) {
                    self[eventName] = function(func) {
                        if (func && typeof func === 'function') {
                            bind.call(self, eventName, func);
                        } else {
                            var args = Array.prototype.slice.call(arguments);
                            args.unshift(eventName);
                            trigger.apply(self, args);
                        }
                    };
                });
            }
        }
        EventProducer.bind = EventProducer.prototype.bind = bind;
        EventProducer.unbind = EventProducer.prototype.unbind = unbind;
        EventProducer.trigger = EventProducer.prototype.trigger = trigger;
        EventProducer.getBinder = EventProducer.prototype.getBinder = getBinder;

        return EventProducer;
    })();

    $.extend(namespace, {
        EventProducer : EventProducer
    });
})(FECRU.MIXINS, AJS.$);
;
/* END /2static/script/fecru/mixins.js */
/* START /2static/script/fecru/modernise.js */
/* modernise.js - holds code needed for older, non-IE browsers.
* */




/* Properties:

 //run a get function:
 var x = theObject.thePropertyName;
 //run a set function:
 theObject.thePropertyName = value;

 The W3C standard is to use
 Object.defineProperty(theObject, "thePropertyName", {
    get : function() { return myValue; },
    set : function(value) { myValue = value; }
 });

 Older versions of FF (and others) instead use a custom function name (__define{G|S}etter__ was the
 de facto standard).  When Object.defineProperty doesn't exist, we define it using those custom functions:

 theObject.__defineGetter__("thePropertyName", function() { return myValue; });
 theObject.__defineSetter__("thePropertyName", function(value) { myValue = value; });

 Lowest browser versions that support Object.defineProperty:
    Firefox 4, (only one not there yet?)
    IE 8,
    Chrome (any),
    Safari 5
 This code can be removed once those are our lowest supported versions.
 */
if (!Object.defineProperty) {
    Object.defineProperty = function(obj, prop, descriptor) {
        if (descriptor.value) {
            obj[prop] = descriptor.value;
        } else {
            if (obj.__defineGetter__ && descriptor.get) {
                obj.__defineGetter__(prop, descriptor.get);
            }
            if (obj.__defineSetter__ && descriptor.set) {
                obj.__defineSetter__(prop, descriptor.set);
            }
        }
    };
};
/* END /2static/script/fecru/modernise.js */
/* START /2static/script/fecru/ajax.js */
window.FECRU = window.FECRU || {};

FECRU.AJAX = (function($, window) {

    /**
     * We need to distinguish between two cases. A user clicking away to another page, cancelling an Ajax request. In that case we
     * want to do nothing. When the server refuses a connection, we want to display an error message.
     */
    var unloaded = false;
    $(window).bind('beforeunload', function() {
        unloaded = true;
    });

    /**
     * Factory method that creates a new SequentialAjaxExecutor instance.
     */
    function createSequentialExecutor() {
        return new SequentialAjaxExecutor();
    }

    /******************************************************************
     ******************** AJAX METHODS ********************************
     ******************************************************************/

    /**
     * This class represents a serial channel for ajax calls. An ajax call that
     * is submitted only gets run after the previous call has returned from the
     * server.
     * This is used by Crucible comments: when you click "post" while autosave
     * is waiting for the server to create and return the new commentId, the
     * Post action is queued and automatically gets evaluated after autosave
     * has returned (and populated the js comment model with the new commentId).
     */
    function SequentialAjaxExecutor() {

        var busy = false;
        var $jobs = $('<div></div>');   // private var to SequentialAjaxExecutor, never inserted into the document.

        /**
         * Makes an AJAX call, similar to FECRU.AJAX.ajaxDo(), but with the exception that
         * only one call can be active at any time. When this method is called
         * a second time, while the previous call has not yet finished, the new
         * call will be scheduled and executed automatically when the previous
         * call finishes.
         *
         * @param url
         * @param params
         * @param onCompleteFunc
         */
        this.executeAjax = function(url, params, onCompleteFunc, noDialog) {
            this.executeAjaxJob({
                url : url,
                params : params,
                callback : onCompleteFunc,
                noDialog : noDialog
            });
        };

        /**
         * Similar to SerializedAjaxDispatcher.executeAjax(), but takes a hash with
         * functions that allow callers to evaluate their url and query params
         * just-in-time.
         *
         * @param job
         */
        this.executeAjaxJob = function(job) {
            if (busy) {
                var dispatcher = this;
                $jobs.queue(function() {
                    dispatcher.executeAjaxJob(job);
                });
            } else {
                busy = true;
                ajaxDo(
                        $.isFunction(job.url) ? job.url() : job.url,
                        $.isFunction(job.params) ? job.params() : job.params,
                        function(resp) {
                            try {
                                if (job.callback) {
                                    job.callback(resp);
                                }
                            } finally {
                                busy = false;
                                $jobs.dequeue();
                            }
                        },
                        $.isFunction(job.noDialog) ? job.noDialog() : job.noDialog);
            }
        };

        this.isPending = function() {
            return busy;
        };
    }

    function ajaxDo(url, params, onCompleteFunc, noDialog) {
        ajaxUpdate(url, params, null, onCompleteFunc, noDialog);
    }

    /**
     * Makes a Ajax request with the specified parameters and runs the onCompleteFunc
     * function when the call returns. Optionally, a DOM element can be passed in
     * which will be updated with the payload of the Ajax response (the JSON response
     * object must contain a "payload" member.
     *
     * noDialog is optional: if falsy, the error message dialog is shown on error.
     * If it is a callback, it is invoked on error instead of showing the dialog.
     * Otherwise, if it is truthy, the dialog is not shown on error and no action
     * is taken. It makes sense to not report an error when the Ajax action is triggered by a timer, rather than by a
     * user action, or where the failure of the Ajax action doesn't affect what the user is doing. (e.g. ?)
     * TODO any other cases where we should not show an error?
     */
    function ajaxUpdate(url, params, elementIdToUpdate, onCompleteFunc, noDialog) {
        // Implemementation note:
        // jQuery does an eval(req.responseText) for us internally. We have an onSuccess callback to
        // intercept this, otherwise we end up eval'ing the response text twice (which is poor form).
        // The onComplete callback gives us access to the XmlHttpRequest and allows us to do extra
        // error handling if required.

        var data = false;
        var onSuccess = function(response) {
            data = response;
        };

        var onComplete = function(req) {
            var cleanUpOnFail = function () {
                if (onCompleteFunc) {
                    onCompleteFunc({ worked: false });  //allow function to clean up
                }
            };
            try {
                
                if (req.status === 0) {
                    if (!unloaded) {
                        cleanUpOnFail();
                        if (!noDialog) {
                            showServerUnreachableBox();
                        }
                    }
                    //else this was caused by user navigating away, ignore it.
                    return;
                }
                if (!data || !isECMA(req)) {
                    noDialog || displayUnexpectedResponse(req);
                    cleanUpOnFail();
                    return;
                }

                var $updateMe;
                if (elementIdToUpdate) {
                    $updateMe = $("#"+elementIdToUpdate);
                }

                if (data.hasOwnProperty("worked") && !data.worked) {
                    if (!noDialog) {
                        appendErrorResponse(data.errorMsg);
                        appendCredentialsRequests(data.credentialsRequired);
                        if (data.userError) {
                            showUserErrorBox();
                        } else {
                            showErrorBox();
                        }
                    }
                } else if ($updateMe && $updateMe.length === 1) {
                    if ($updateMe.is(':input')) {
                        $updateMe.val(data.payload);
                    } else {
                        $updateMe.html(data.payload);
                    }
                }
                if (onCompleteFunc) {
                    onCompleteFunc(data);
                }
            } catch (e) {
                console && console.error('ajaxUpdate failure: ' + e);
                displayErrorInResponse(e);
                cleanUpOnFail();
            }
        };

        $.ajax( {
            type: "post",
            url: url,
            data: params,
            dataType: "json",
            traditional: true, // serialize array params as rev=1&rev=2 rather than rev[]=1&rev[]=2
            success: onSuccess,
            complete: onComplete,
            error: noDialog ? ($.isFunction(noDialog) ? noDialog : NOOP) : ajaxFailure
        } );
        return false;
    }



    var NOOP = function () {};

    var ajaxFailure = function (req, textStatus, errorThrown) {
        try {
            if (!isECMA(req)) {
                displayUnexpectedResponse(req);
                return;
            }
            var resp = eval('(' + req.responseText + ')');
            var errorShown = false;
            if (!resp.worked || resp.errorMsg) {
                appendErrorResponse(resp.errorMsg);
                errorShown = true;
            }
            if (errorThrown) {
                appendErrorResponse(errorThrown);
                errorShown = true;
            }
            if (!errorShown) {
                appendErrorResponse("Unknown error");
            }
            showErrorBox();
        } catch (e) {
            console && console.error('ajaxFailure: ' + e);
            displayErrorInResponse(e);
        }
    };

    /******************************************************************
     ******************** HELPER METHODS ******************************
     ******************************************************************/

    var isECMA = function (req) {
        return isContentType(req, /^application\/(ecmascript|json)/);
    };

    var isHTML = function (req) {
        return isContentType(req, /^text\/html/);
    };

    var isContentType = function (req, targetType) {
        var respContentType = req.getResponseHeader("Content-Type");
        return respContentType != null && respContentType.match(targetType);
    };

    var isHTMLFrag = function (req) {
        var responseText = req.responseText;
        return responseText != null && !responseText.match(/<body/i);
    };

    //TODO use FECRU.htmlEscape || AJS.escapeHtml
    var ourHtmlEscape = function (input) {
        try {
            return input.replace(/&/g, '&amp;').
                    replace(/>/g, '&gt;').
                    replace(/</g, '&lt;').
                    replace(/"/g, '&quot;');
        } catch(e) {
            console && console.error('escape failure: ' + e);
            return input.message || e.message;
        }
    };

    function startSpin(id, className, replace) {
        className = className ? className + " spinner" : "spinner";

        var $el = typeof(id) == 'object' ? $(id) : $("#"+id);
        if ($el.length === 1) {
            var $spinner = $(document.createElement("span"));
            $spinner.addClass(className)
                    .html("<img src='" + FECRU.pageContext + "/" + FECRU.staticDirectory + "/2static/images/blank.gif'>");
            if (replace) {
                $el.replaceWith($spinner);
            } else {
                $spinner.insertAfter($el);
            }
            return true;
        }
        return false;
    }

    function stopSpin(id) {
        var $el = typeof(id) == 'object' ? $(id) : $("#"+id);

        if ($el.length === 1) {
            $el.nextAll(".spinner").remove();
        }
    }



    /******************************************************************
     ******************** ERROR HANDLING ******************************
     ******************************************************************/

    function checkError(resp) {
        return resp.error;
    }

    function appendErrorResponse(error, noEscape) {
        if (error) {
            appendErrorMessage(error + (error.stack ? ":\n" + error.stack : ""), noEscape);
        }
    }

    function appendCredentialsRequests(credentialsRequired) {
        if (credentialsRequired) {
            for (var i=0;i<credentialsRequired.length;i++) {
                var creds = credentialsRequired[i];
                var msg = "Click <a href='" + creds.authUrl + "' target='_new'>here</a> to authenticate with " + creds.name;
                appendErrorMessage(msg, true);
            }
        }
    }

    function appendErrorMessage(errorMsg, noEscape) {
        if (errorMsg) {
            var $err = $('#errorResponses'),
                    errorCount = $err.find('.errorResponse').length,
                    escaped = (noEscape) ? errorMsg : ourHtmlEscape(errorMsg);

            var $existingErrors = $('#errorResponses .errorResponse');
            for (var i = 0, len = $existingErrors.length; i < len; i++) {
                if (escaped == $($existingErrors[i]).html()) {
                    return; // dont show an error if we have already seen it
                }
            }

            $err.show()
                .append($('<div class="errorResponse-count">Error' + (errorCount ? (' ' + ++errorCount) : '') + '</div>'))
                .append($('<div class="errorResponse"></div>').html( escaped ));
        }
    }

    function appendNotificationMessage(message) {
        if (message) {
            var $err = $('#errorResponses'),
                $msg = $('<div class="notification-box-message"></div>');
            if (message.jquery) {
                $msg.append(message);
            } else {
                $msg.text(message);
            }
            $err.append($msg);
        }
    }

    var dialog = 0;

    function showErrorBox() {
        showNotificationBox('An error has occurred');
        $('#errorDetails').show();
    }

    function showUserErrorBox() {
        showNotificationBox('Cannot perform requested action');
        $('#errorDetails').show();
    }

    function showServerUnreachableBox() {
        showNotificationBox('The server is not responding.');
        $('#errorDetails').show();
    }

    function showNotificationBox(title, severity) {
        severity = severity || "error";
        var util = CRU.UTIL;

        if (dialog == 0) {
            dialog = FECRU.DIALOG.create(440, 330, "error-dialog");
            dialog.addHeader(title)
                .addPanel("Information", $('#errorBox'))
                .addButton("Reload", function() { window.location.reload(); }, "reload-button")
                .addButton("Close", function() {
                    $('#errorResponses .errorResponse').remove();
                    $('#errorResponses .errorResponse-count').remove();
                    dialog.hide();
                }, "close-button");
        }
        var $dialog = $('#error-dialog');
        if (severity === 'error') {
            $dialog.addClass('error');
            $dialog.removeClass('message');
        } else if (severity === 'message' && !$dialog.hasClass('error')) {
            $dialog.addClass('message');
            $dialog.find(".errorResponse-count").hide();
        }

        if (!util.isAnyDialogShowing()) {
            if (util.isAjaxDialogSpinning()) {
                util.stopAjaxDialogSpin();
            }
            dialog.show();
        }
        return false;
    }

    function displayUnexpectedResponse(req) {
        if (isHTML(req) && isHTMLFrag(req)) {
            appendErrorResponse(req.responseText);
            showErrorBox();
        }
        else if (!req.responseText) {
            //no-op here: no responseText means most likely server is gone offline, which is handled in onComplete
        }
        else {
            var resp = eval('(' + req.responseText + ')');

            if (!resp.errorMessages[0]) {
                resp.errorDetails = "No further details were provided.";
            }
            else {
                resp.errorDetails = resp.errorMessages.join("<br>");
            }

            var startsWithVowel = resp.errorName && (-1 !== "aeiou".indexOf(resp.errorName.charAt(0).toLowerCase()));

            var currentError = [
                    (resp.errorName ? (startsWithVowel ? "An " : "A ") + resp.errorName : "An error"),
                    " was encountered<br><br>",
                    resp.errorDetails
                ].join(""),
                noEscape = true
            ;

            appendErrorResponse(currentError, noEscape);
            showErrorBox();
        }
    }

    function displayErrorInResponse(err) {
        appendErrorResponse(err);
        showErrorBox();
    }

    return {
        createSequentialExecutor : createSequentialExecutor,

        ajaxDo : ajaxDo,
        ajaxUpdate : ajaxUpdate,

        startSpin : startSpin,
        stopSpin : stopSpin,

        checkError : checkError,

        appendErrorResponse : appendErrorResponse,
        appendErrorMessage : appendErrorMessage,
        showErrorBox : showErrorBox,

        appendNotificationMessage : appendNotificationMessage,
        showNotificationBox : showNotificationBox
    };
})(AJS.$, this);
;
/* END /2static/script/fecru/ajax.js */
/* START /2static/script/fecru/browse.js */
window.FECRU = window.FECRU || {};
FECRU.BROWSE = {};

(function() {
    /**
     * Open a folder, given the node of its span, and then run a post function. Opening the node may mean loading a
     * subtree via Ajax -- if this happens, the post function will be run after the subtree has loaded.
     *
     * @param $node -- a jQuery wrapper around the span tag containing the folder name (which is a sibling of the ul tag containing its children)
     * @param postFn -- a function of no arguments to run after opening the node
     * @param actionName -- an optional string naming the action to call to load a subtree
     * @param ajaxArgsFn -- an optional function to call to provide extra parameters to the subtree load fn
     */
    var toggleFolder = function($node, postFn, pathLinkFn, fileLinkFn, actionName, ajaxArgsFn) {
        if ($node.hasClass("unfilled")) {
            $node.removeClass("unfilled");
            var liId = $node.closest("li.tree-li").attr('id');
            var treeData = AJS.$("#tree-root").data("extraAttrs")[liId];
            var ajaxParameters = {
                path: treeData.path,
                repName: treeData.repname,
                baseUrl: treeData.baseurl,
                noFiles: treeData.nofiles
            };
            var selectedPath = AJS.$("#selectedDirTreeNode").children("input[name='selectedPath']").val();
            if (selectedPath) {
                ajaxParameters.selectedPath = selectedPath;
            }
            if (ajaxArgsFn) {
                var args = ajaxArgsFn($node);
                for (k in args) {
                    ajaxParameters[k] = args[k];
                }
            }
            $node.after(AJS.$("<span class='dirlistSpinner'>&nbsp;</span>").show());
            $node.removeClass("closed").addClass("open");
            var params = AJS.$("#queryStrSuffix").val();
            if (params) {
                ajaxParameters.queryStrSuffix = params;
            }

            if (!actionName) {
                actionName = "loadSubTree";
            }
            if (AJS.$('#includeBranchHeadParam').length) {
                ajaxParameters["includeBranchHeadParam"] = true;
            }
            FECRU.AJAX.ajaxDo(FECRU.pageContext + "/json/fe/" + actionName + ".do", ajaxParameters, function(resp) {
                if (resp.worked) {
                    // use 'clean' to avoid the regex that jQuery uses to distinguish HTML from ids
                    var replacement = AJS.$.clean([resp.payload], document);
                    $node.parent().replaceWith(replacement);
                    postFn();
                }
            }, false);
        } else {
            var oldClass = "closed";
            var newClass = "open";
            if ($node.hasClass("open")) {
                oldClass = "open";
                newClass = "closed";
            }
            $node.removeClass(oldClass).addClass(newClass);
            $node.siblings("ul." + oldClass).removeClass(oldClass).addClass(newClass);
            postFn();
        }
    };

    var moveSelectedDirTreeIds = function($newLink) {
        AJS.$("#selectedDirTreeNode").attr("id", "");
        AJS.$("#selectedDirTreeLink").attr("id", "");
        $newLink.closest("span").attr("id", "selectedDirTreeNode");
        $newLink.attr("id", "selectedDirTreeLink");
    };

    /**
     * Open the parents of this link and set the selected ids
     * @param $linkNode the link in the directory tree which is selected.
     */
    FECRU.BROWSE.selectLink = function($linkNode, folderToggledFn, pathLinkFn) {
        var $span = $linkNode.parent();
        var done = function() {
            folderToggledFn($linkNode);
        };
        if ($span.hasClass("closed")) {
            FECRU.AJAX.startSpin(AJS.$("#filebox"), "", true); // Get's stopped when #fileResults has html() called
            toggleFolder($span, done, pathLinkFn);
        } else {
            done();
        }
        moveSelectedDirTreeIds($linkNode);
    };

    FECRU.BROWSE.setupDirectoryTree = function(pathLinkFn, fileLinkFn, actionName, ajaxArgsFn) {
        AJS.$(document).delegate("span.tree", "click", function(event) {
            if (AJS.$(event.target).is("a")) {
                return true; // let the link do its job
            }
            toggleFolder(AJS.$(this), function() {}, pathLinkFn, fileLinkFn, actionName, ajaxArgsFn);
            event.stopPropagation();
            return false;
        });
        if (pathLinkFn) {
            AJS.$(document).delegate("#navigation-tree a.pathLink", "click", function(event) {
                return pathLinkFn(event);
            });
        }
        if (fileLinkFn) {
            AJS.$(document).delegate("#navigation-tree a.fileLink", "click", function(event) {
                return fileLinkFn(event);
            });
        }
    };

    FECRU.BROWSE.initDirectoryTree = function(pathLinkFn, fileLinkFn, actionName, ajaxArgsFn) {
        FECRU.BROWSE.setupDirectoryTree(pathLinkFn, fileLinkFn, actionName, ajaxArgsFn);
        if (window.FE) {
            FE.setupPanes();
        }
    };
})();
;
/* END /2static/script/fecru/browse.js */
/* START /2static/script/fecru/dialog.js */
window.FECRU = window.FECRU || {};
FECRU.DIALOG = {};

(function ($, namespace) {
    var safeDimensions = function (maxWidth, maxHeight, margin) {
        margin = margin || 50;
        return {
            height: Math.min($(window).height() - margin, maxHeight),
            width: Math.min($(window).width() - margin, maxWidth)
        };
    };

    var $container = null;
    function initAjaxContainer() {
        $container = $('<div id="ajax-dialog-container"></div>');
        $(document).ready(function () {
            $('body').append($container);
        });
    }

    function create(maxWidth, maxHeight, id, options) {
        var dimensions = safeDimensions(maxWidth, maxHeight),
            dimensionsAndId = {
                width : dimensions.width,
                height : dimensions.height,
                id : id
            };

        options = options ?
            $.extend({}, options, dimensionsAndId) :
            dimensionsAndId;
        var dialog = new AJS.Dialog(options);
        // Add the height and width of the dialog as properties of the object
        $.extend(dialog, {width: dimensions.width, height: dimensions.height});
        return dialog;
    }

    function triggerAjaxDialogLoaded() {
        $(document).trigger('ajax-dialog-loaded');
    }

    function ajaxDialog(maxWidth, maxHeight, data, id) {
        var $container = getAjaxDialogContainer();

        var dialog = FECRU.DIALOG.create(maxWidth, maxHeight, id);
        $.each(
            $.extend({ dialog: dialog }, data),
            function (k, v) {
                $container.data(k, v);
            }
        );
        return dialog;
    }

    function getAjaxDialogContainer() {
        if (!$container) {
            initAjaxContainer();
        }
        return $container;
    }

    $.extend(namespace, {
        create : create,
        ajaxDialog : ajaxDialog,
        triggerAjaxDialogLoaded : triggerAjaxDialogLoaded,
        getAjaxDialogContainer : getAjaxDialogContainer
    });
})(AJS.$, FECRU.DIALOG);
;
/* END /2static/script/fecru/dialog.js */
/* START /2static/script/fecru/gwtutil.js */
window.FECRU = window.FECRU || {};

FECRU.GWT = {
    /**
     * Scroll to the element identified by selector
     * in the current window.
     * 
     * @param selector selector to get the element
     */
    scrollToId: function(selector) {
        AJS.$(window).scrollTo(AJS.$(selector));
    },
    /**
     * Scroll to the element identified by selector, scrolling
     * the element selected by container.
     *
     * @param selector selector to get the element
     * @param container scrollable container
     */
    scrollToIdInContainer: function(selector, container) {
        AJS.$(container).scrollTo(AJS.$(selector));
    }
};
;
/* END /2static/script/fecru/gwtutil.js */
/* START /2static/script/fecru/hover.js */
window.FECRU = window.FECRU || {};
FECRU.HOVER = (function() {
    var opts = {
        onHover: true,
        fadeTime: 200,
        hideDelay: 500,
        showDelay: 220,
        width: 300,
        offsetX: -4,
        offsetY: 10,
        container: "body",
        cacheContent:false,
        useLiveEvents : true,
        initCallback: function() {
            //this.popup.refresh();
        }
    };
    var respCache = {};
    var CACHE_FOREVER = -1;
    var displayHandlerDefaultOpts = {
        createUrl:function() {
            throw new Error("must provide createUrl");
        },
        createCacheKey:function() {
            throw new Error("must provide createCacheKey");
        },
        createParams:function() {
            throw new Error("must provide createParams");
        },
        minutesToCache:CACHE_FOREVER,
        cacheType:"__all",
        createTemplateContent:function() {
            return "";
        }
    };
    /**
     * createDisplayHandler returns a function that handles the display of the hover popup, and is used as the display handler
     * to be passed into AJS.InlineDialog as the url parameter (which is overloaded to take a function like this one).
     * @param params The named parameter needs the following properties:
     * @property createUrl : a function that accepts a jquery elem of the trigger of the hover, and returns the url to get
     * the content of the hover.
     * @property createCacheKey : a function that accepts a jquery elem of the trigger, and returns a unique string key for
     * this hover. Used to cache the result of the call created by createUrl
     * @property createParams : a function that accepts a key (created from createCacheKey) and a jquery elem of the trigger,
     * and returns a hash of the parameters to be passed along in the ajax request to the url created by creatUrl.
     * @optionalproperty minutesToCache : number of minutes to cache the result of the call to the url created by createUrl.
     * Defaults to CACHE_FOREVER
     * @optionalproperty cachetype : a unique string that identifies the type of hover being shown. Used by invalidateCache() to
     * clear the cache of this type. can be null.
     * @optionalproperty createExtraParams : a function that accepts the trigger and returns a map of parameters to be passed to the server
     * @optionalproperty createTemplateContent : a function that accepts a key, and optionally the trigger and returns a string to be
     * placed in the spinner template.
     */
    var createDisplayHandler = function(params) {
        var options = AJS.$.extend({}, displayHandlerDefaultOpts, params);
        var minutesToCache = options.createUrl,
                cachetype = options.minutesToCache,
                createTemplateContent = options.createTemplateContent;

        return function ($contentDiv, mouseOverTrigger, showPopup) {
            var $trigger = AJS.$(mouseOverTrigger);
            var url = options.createUrl($trigger);
            var key = options.createCacheKey($trigger);
            var cache = respCache[cachetype] || {};
            var cacheHit = cache[key] || {isHit:false};
            var lastFetchTime = cacheHit.lastFetchTime || 0;
            var cacheTimedOut = (minutesToCache !== CACHE_FOREVER) &&
                                ((new Date().getTime() - lastFetchTime) > (minutesToCache * 1000 * 60));
            if (key) {
                $contentDiv.data("targetToDisplay", {key:key});
                var error = function(xmlHttpRequest, textStatus, errorThrown) {
                    $contentDiv.html(escape(textStatus) + "<br>" + escape(errorThrown ? errorThrown : ""));
                };
                var delayedShowSpinner;
                var done = function(resp) {
                    if (resp.worked) {
                        if (delayedShowSpinner) {
                            clearTimeout(delayedShowSpinner);
                        }
                        if ($contentDiv.data("targetToDisplay").key == key) {
                            $contentDiv.html(resp.html);
                            // show the popup if the spinner wasn't shown (either canceled or not supplied)
                            if (!createTemplateContent || delayedShowSpinner) {
                                showPopup();
                            }
                        }
                        // save result to cache
                        cacheHit.resp = resp;
                        cacheHit.lastFetchTime = new Date().getTime();
                        cacheHit.isHit = true;
                        cache[key] = cacheHit;
                        respCache[cachetype] = cache;
                    } else {
                        $contentDiv.html('<div class="hoverpopup">' + resp.errorMsg + '</div>');
                    }
                };
                if ((!cacheHit.isHit) || cacheTimedOut) {
                    AJS.$.ajax({url: url,
                        data:options.createParams(key, $trigger),
                        type:"GET",
                        dataType:"json",
                        success: done,
                        error: error
                    });
                    // show the spinner if the template is provided, delaying it for a bit
                    if (createTemplateContent) {
                        delayedShowSpinner = setTimeout(function () {
                            if ($contentDiv.data("targetToDisplay").key == key) {
                                $contentDiv.html(getSpinnerTemplate(createTemplateContent(key, $trigger)));
                                showPopup();
                            }
                        }, 150);
                    }
                } else {
                    $contentDiv.html(cacheHit.resp.html);
                    showPopup();
                }
            }
        };
    };

    var invalidateCache = function(type, key) {
        if (!key) {
            respCache[type] = undefined;
        } else if (respCache[type]) {
            respCache[type][key] = undefined;
        }
    };

    var addAllLinkPopups = function() {
        addJiraLinkPopups();
        addCruLinkPopups();
        addCsLinkPopups();
        addUserLinkPopups();
        addDeletedUserLinkPopups();
    };

    var addJiraLinkPopups = function() {
        var jiraShowDelay = 300;
        addLinkPopups({
            linkSpanClass:"jira-hover-trigger",
            url:FECRU.pageContext + '/json/action/issue-tooltip.do',
            showDelay:jiraShowDelay
        });
    };

    var addCruLinkPopups = function() {
        addLinkPopups({
            linkSpanClass:"crulinkspan",
            url:FECRU.pageContext + '/json/cru/tooltipdata',
            keyParser: function($trigger) {
                var $permaId = $trigger.find('input.permaId');
                if ($permaId.length == 1) {
                    return $permaId.val();
                }
                return $trigger.find('a').text();
            }
        });
    };

    var addCsLinkPopups = function() {
        var changesetKeyPattern = /^(\/\/([^/]+)\/)(.+)$/;
        addLinkPopups({
            linkSpanClass: "cslinkspan",
            url: FECRU.pageContext + '/json/action/cstooltipdata.do',
            keyParser: function ($trigger) {
                return '//' + $trigger.find('input.repname').val() + '/' + $trigger.find('input.csid').val();
            },
            createTemplateContent: function(key) {
                var matches = changesetKeyPattern.exec(key);
                if (matches) {
                    var changesetId = matches[3];
                    var repository = matches[2];
                    //abbrev. the id if too long
                    changesetId = changesetId.length > 10 ? changesetId.substring(0, 6) + "..." : changesetId;
                    return "changeset " + changesetId + " in " + repository;
                } else {
                    return "changeset";
                }
            },
            createExtraParams: function ($trigger) {
                return {
                    "repname": $trigger.find('input.repname').val(),
                    "csid": $trigger.find('input.csid').val()
                };
            }
        });
    };

    var addLinkPopups = function(params) {
        var defaults = {
            //the css class to bind the hover trigger to
            linkSpanClass:'',
            //the url to request the content of the trigger
            url:'',
            //how much to delay showing of the hover when triggered
            showDelay: opts.showDelay,
            //given the trigger jquery elem, return a unique string usable as a key for the cache
            keyParser:function ($trigger) {
                return $trigger.text();
            },
            //given the key and trigger, return something to display in the spinner message. can be an empty string.
            createTemplateContent : function(key, $trigger) {
                return key;
            }
        };
        var options = AJS.$.extend({}, defaults, params);
        var linkSpanClass = options.linkSpanClass;
        var displayHandler = createDisplayHandler({
            createUrl:function() {
                return options.url;
            },
            createCacheKey:function($trigger) {
                var $keylink = AJS.$("a", $trigger);
                if ($keylink.length === 0) {
                    $keylink = $trigger;
                }
                var key = options.keyParser($keylink);
                key = AJS.$.trim(key);
                return key;
            },
            createParams:function(key, $trigger) {
                var extraParams = (options.createExtraParams && options.createExtraParams($trigger)) || {};
                return AJS.$.extend(false, extraParams, {key:key});
            },
            cacheType:linkSpanClass + "cache",
            createTemplateContent:options.createTemplateContent
        });
        var hoverOpts = AJS.$.extend(false, opts, {showDelay: options.showDelay});
        AJS.InlineDialog("." + linkSpanClass, linkSpanClass + "-popup", displayHandler, hoverOpts);
    };

    var addUserLinkPopups = function() {
        var getHref = function($trigger) {
            return $trigger.attr('href') || $trigger.parent().attr('href'); //sometimes the trigger is the child
        };
        var displayHandler = createDisplayHandler({
            createUrl: function($trigger) {
                return getHref($trigger);
            },
            createCacheKey:function($trigger) {
                return getHref($trigger);
            },
            createParams:function() {
                return {ajax :"true"};
            },
            cacheType:'userlinks',
            createTemplateContent:function(key, $trigger) {
                var $linkText = $trigger.hasClass('linkText') ? $trigger : $trigger.find(".linkText");
                if ($linkText.length === 0) {
                    //if the linkText doesnt exist, which does happen for avartars with no displayed name
                    return key.replace(/^.*\//, "");//remove all but the username
                } else {
                    return $linkText.text();
                }
            }
        });
        AJS.InlineDialog("a.userorcommitter,a.userorcommitter-parent .linkText", "user-hover-inline-dialog", displayHandler, opts);
    };

    var addDeletedUserLinkPopups = function() {
        var hoverContent = function($contents, trigger, showPopup) {
            var $trigger = AJS.$(trigger);
            $contents.html(
                "<div class='user-hover-info'>"+
                    "<div class='user-hover-avatar'>"+
                        "<img height='48' width='48' alt='Deleted User' src='" + FECRU.pageContext + "/avatar/deleted' />"+
                    "</div>"+
                    "<div class='user-hover-details'>"+
                    "<h4><span class='linkText'></span></h4>"+
                    "<em>(deleted user)</em>"+
                "</div>"
            );
            var usernameSpan = $trigger.find(".hidden-username");
            if (usernameSpan) {
                $contents.find("span.linkText").text(usernameSpan.text());
            }
            showPopup();
        };
        AJS.InlineDialog("a.deleteduser,a.deleteduser-parent .linkText", "deleted-user-hover-inline-dialog", hoverContent, opts);
    };

    var getSpinnerTemplate = function(content) {
        // Don't add the content directly into the spinner, as it may contain unescaped HTML content.
        // The workaround is to create a jQuery object and call .text(), and then return the html of the constructed object.
        var $elem = AJS.$('<div class="hoverpopup-throb jirahoverpopup-throb">' +
                '<img src="' + FECRU.pageContext + '/' + FECRU.staticDirectory +
                '/2static/images/spinner_003366.gif" alt="Retrieving details">' +
                '<em class="hoverpopup-throbber-text">Retrieving ' +
                '<span class="hoverpopup-throbber-text-content">' +
                '</span>...</em></div>');
        $elem.find(".hoverpopup-throbber-text-content").text(content);
        return $elem.html();
    };

    return {
        addAllLinkPopups: addAllLinkPopups,
        invalidateCache : invalidateCache,
        CACHE_FOREVER: CACHE_FOREVER
    };
})();
;
/* END /2static/script/fecru/hover.js */
/* START /2static/script/fecru/profile.js */
window.FECRU = window.FECRU || {};
if (!FECRU.PROFILE) {
    FECRU.PROFILE = (function() {
        var makeDialogFor = function($profileSettingsLink) {
            var windowWidth = AJS.$(window).width(),
                windowHeight = AJS.$(window).height(),
                width = (windowWidth < 1000) ? windowWidth - 120 : 800,
                height = (windowHeight < 700) ? windowHeight - 100 : 700,
                HEADER_HEIGHT = 43, //height of the dialog header, in px
                BUTTON_HEIGHT = 44, //height of the buttons at bottom of dialog, in px.
                BODY_PADDING = 20, // needs to be the same as .aui-dialog .dialog-panel-body declaration in dialog.css
                IFRAME_SPACING = 3, // under html5 the iframe gets arbtirary following spacing, and this removes the scrollbar it causes
                OUTERHEIGHT = HEADER_HEIGHT + BUTTON_HEIGHT + BODY_PADDING + IFRAME_SPACING, // 110
                iframeHeight = height - OUTERHEIGHT,
                settingsDialog = FECRU.DIALOG.create(width, height, 'fecru-profile-settings-dialog'),
                deepProfileSettingsLink = $profileSettingsLink.attr("href") || FECRU.pageContext + "/profile";

            // hack: we're adding a random number to the iframe ID to work
            // around webkit bug: 24078 (http://lists.macosforge.org/pipermail/webkit-unassigned/2009-February/100941.html)
            var $iframe = AJS.$("<iframe id='fecru-iframe-" + (Math.ceil(Math.random() * 1000)) + "' frameborder='0' src='" + deepProfileSettingsLink + "' style='width:100%;height:" + (iframeHeight) + "px' ></iframe>");

            settingsDialog.addHeader("Settings");
            settingsDialog.addPanel("Display", $iframe);
            settingsDialog.addButton("Close", function (dialog) {
                dialog.hide();

                //todo: fix up reloading?
                if (getDialogURL()) {
                    // Remove "dialog" parameter before reloading
                    var topURL = window.location.href;
                    topURL = topURL.replace(/\?dialog=[^&]*/, "?");
                    topURL = topURL.replace(/&dialog=[^&]*/, "");
                    topURL = topURL.replace(/\?$/, "");
                    window.location.replace(topURL);
                } else {
                    window.location.reload();
                }
            });
            return settingsDialog;
        };

        AJS.$(document).ready(function () {
            var toggleMappingSubmitButton = function() {
                var disable = (AJS.$("#repositoryDropdown").attr("selectedIndex") == 0);
                AJS.$("#addMappingButton").attr("disabled", disable);
            };
            AJS.$("#repositoryDropdown").change(toggleMappingSubmitButton);
            toggleMappingSubmitButton();    // set initial state

            var settingsDialog;
            //todo may be use live events?
            var $profileSettingsLink = AJS.$("a.dialog-settings").click(function(e) {
                e.preventDefault();
                if (!settingsDialog) {
                    settingsDialog = makeDialogFor($profileSettingsLink);
                }
                settingsDialog.show();
            });

            var $form = AJS.$('form.autosubmit');
            $form.find('input,select').change(function () {
                var params = $form.serialize();
                var action = $form.attr('action');
                var $spinner = $form.find('.edit-settings-spinner').show();
                var saved = function () {
                    setTimeout(function() {
                        $spinner.hide();
                    }, 500); //show the spinner for slightly longer, so that the feedback is visible for longer
                };

                FECRU.AJAX.ajaxDo(action, params, saved);
            });

            $form.find('#clear-ignored-applinks').click(function () {
                var $spinner = AJS.$(".clear-ignored-applinks-spinner").show();
                FECRU.UAL.clearIgnoredAppLinks(function() {
                    setTimeout(function() {
                        $spinner.hide();
                        window.location.reload();
                    }, 500); //show the spinner for slightly longer, so that the feedback is visible for longer
                });
            });

            // If there's a dialog argument on our URL, open that dialog.
            var dialogURL = getDialogURL();
            if (dialogURL) {
                settingsDialog = makeDialogFor(AJS.$("<a href='" + FECRU.pageContext + dialogURL + "' />"));
                settingsDialog.show();
            }

        });

        function getDialogURL() {
            // Don't match URLs which are login redirects
            if (location.search.match(/origUrl/)) {
                return null;
            }
            var matches = /[?&]dialog=([^&]*)/.exec(location.search);
            if (matches) {
                return decodeURIComponent(matches[1]);
            }
            return null;
        }

        return true; //flag to stop multiple calls which adds multiple dialog boxes.
    })();
}
;
/* END /2static/script/fecru/profile.js */
/* START /2static/script/fecru/rss.js */
window.FECRU = window.FECRU || {};

FECRU.RSS = (function() {
    var makeDialogFor = function($Link) {
        var deepRSSLink = $Link.attr("href");
        var dlg = FECRU.DIALOG.create(800, 500, 'rss-settings-dialog');

        var $iframe = AJS.$("<iframe id='fecru-iframe' frameborder='0' src='" + deepRSSLink + "' style='width:100%;height:"+dlg.height+"px'></iframe>");

        dlg.addHeader("Notification Configuration")
           .addPanel("Display", $iframe)
           .addButton("Close", function (dialog) {
                dialog.hide();
                $iframe.attr("src", deepRSSLink);
            });
        return dlg;
    };
    var setupRSSDialog = function () {
        var rssDialog;
        var $rssLink = AJS.$("#dialog-rss").click(function (e) {
            e.preventDefault();
            if (!rssDialog) {
                rssDialog = makeDialogFor($rssLink);
            }
            rssDialog.show();
        });
    };

    AJS.toInit(function() {
        var $context = AJS.$("#rss-form");
        AJS.$("input, select", $context).change(function() {
            $context.submit();
        });
    });

    return {
        setupRSSDialog : setupRSSDialog
    };

})();
;
/* END /2static/script/fecru/rss.js */
/* START /2static/script/fecru/ui.js */
window.FECRU = window.FECRU || {};
FECRU.UI = FECRU.UI || {};

(function() {

    var hasSetDropdowns = false;

    var enabledAnimations = true;
    if (AJS.$.browser.msie && AJS.$.browser.version < 9) {
        enabledAnimations = false;
    }

    FECRU.UI.setDropdowns = function () {
        if (hasSetDropdowns) {
            AJS.log("WARNING: About to set duplicated dropdown live events");
        } else {
            hasSetDropdowns = true;
        }

        var options = {
            selectionHandler: function () {
                // Prevent AUI from reimplementing default browser handling of <a> elements.
                // May need to revisit depending on AUI changes (see CR-FE-1617).
            },
            useLiveEvents : true, //using live meant that hovers will no longer need to perform a dropdown bind on load
            trigger : '.aui-dd-link'
        };

        options.dropDown = ".aui-dropdown-left:not(.aui-dropdown-ajax)";
        options.alignment = "left";
        AJS.dropDown.Standard(AJS.$.extend({}, options));

        options.dropDown = ".aui-dropdown-right:not(.aui-dropdown-ajax)";
        options.alignment = "right";
        AJS.dropDown.Standard(AJS.$.extend({}, options));
    };

    var setupFilterInlineDialog = function(trigger, box, unique, offsetX) {
        var PADDING = 14;
        var $filterBox = AJS.$(box);
        if ($filterBox.length) {
            var options = {
                onHover: false,
                showArrow: true,
                fadeTime: 200,
                hideDelay: null,
                width: $filterBox.width() + PADDING,
                offsetX: offsetX || 0,
                offsetY: PADDING,
                container: "body",
                cacheContent:true,
                ignoredElements:['.ui-datepicker'] //dont want to hide the filter box when selecting dates
            };
            var contentHandler = function ($contentDiv, mouseOverTrigger, showPopup) {
                $filterBox.appendTo($contentDiv).show();
                showPopup();
            };
            return AJS.InlineDialog(trigger, unique, contentHandler, options);
        }
        return null;
    };

    FECRU.UI.filterToggle = function(filters) {
        return setupFilterInlineDialog(".filter-toggle", filters, "filter-inline-dialog", 6);
    };

    /**
     * Setup a delegated event for the (+2 more) links for branches, tags, parents etc.
     * Currently used in the changeset and file history pages
     * @param $container
     */
    FECRU.UI.registerMetadataExpanders = function($container) {
        $container.delegate(".metadata-plus a", "click", function () {
            var $p = AJS.$(this).parent();
            $p.siblings(".hidden").removeClass("hidden");
            $p.hide();
        });
    };

    var linkAugment = function () {
        AJS.$("a[rel='help']").attr("target", "_help");

        // details switch in stream
        // todo delete?
        AJS.$(".details-switch").unbind().click(function () {
            AJS.$(this).toggleClass("details-expanded");

            var $verbose = AJS.$(this).siblings(".details-verbose");
            if ( $verbose.css("display") == "none" ) {//toggle() is broken in IE8, so checking display
                $verbose.show();
            }
            else {
                $verbose.hide();
            }
        });

        // inline hover in stream
        AJS.$(".hover").mouseover(function (event) {
            AJS.$("#hover").addClass("mouseover");
            inlineHover(AJS.$(this), event);
        });

        AJS.$("#hover").mouseout(function() {
            var which = AJS.$(this);
            which.bind("mouseleave", function() {
                AJS.$(which)
                        .removeClass("mouseover")
                        .hide();
            });
        });
        AJS.$("hover-close").click(function() {
            AJS.$(this).hide();
        });
    };

    var formAugment = function (which) {
        if (AJS.$.browser.safari) {//overides type=search
            var searchBox = AJS.$("#quick-search-input");
            if (searchBox.length === 1) {
                searchBox[0].type = "text";
            }
        }
    };

    var $focusedTableRow;
    FECRU.UI.tableRowClick = function (prefix, rowClickFn) {
        AJS.$(document).delegate("#" + prefix + "-table > tbody > tr", "click", function () {
            var clearFocus = function() {
                if ($focusedTableRow) {
                    $focusedTableRow.removeClass(prefix + "-focus");
                }
            };

            var $this = AJS.$(this);
            if ($this.hasClass(prefix + "-focus")) {
                clearFocus();
                $focusedTableRow = null;
            } else {
                clearFocus();
                $this.addClass(prefix + "-focus");
                $focusedTableRow = $this;
                if (rowClickFn) {
                    rowClickFn(this);
                }
            }
        });
    };

    FECRU.UI.tableSort = function (prefix, extractionFn) {
        var params = {};
        if (extractionFn) {
            params['textExtraction'] = extractionFn;
        }
        AJS.$("#" + prefix + "-table").tablesorter(params);
    };

    FECRU.UI.accordion = function () {
        AJS.$(document).delegate('.sidebar-collapse', "click", function() {
            AJS.$('#content').toggleClass('collapsed-sidebar');
            return false;
        }).delegate(".accordion-head", "click", function() {
            var $next = AJS.$(this).next();
            var $parent = AJS.$(this).parent();

            if ($next.is(":hidden")) {
                $parent.addClass("active");
                if (enabledAnimations) {
                    $next.slideDown("fast");
                } else {
                    $next.show();
                }
            }
            else {
                if (enabledAnimations) {
                    $next.slideUp("fast", function () {
                        $parent.removeClass("active");
                    });
                } else {
                    $next.hide();
                    $parent.removeClass("active");
                }
            }
            return false;
        }).delegate("#accordion-toggle", "click", function() {
            var $accordionToggle = AJS.$(this);
            var $accordionContent = $accordionToggle.find(".accordion-content");
            var $parent = AJS.$(this).parent();
            var isExpanded = $accordionToggle.html() === 'expand';
            if (isExpanded) {
                $parent.addClass("active");
                if (enabledAnimations) {
                    $accordionContent.slideDown("fast");
                } else {
                    $accordionContent.show();
                }
            } else {
                 if (enabledAnimations) {
                    $accordionContent.slideUp("fast", function () {
                        $parent.removeClass("active");
                    });
                } else {
                    $accordionContent.hide();
                    $parent.removeClass("active");
                }
            }
            $accordionToggle.html(isExpanded ? 'expand' : 'collapse'); // reverse (it's now "wasExpanded")
            return false;
        });
    };

    var inlineHover = function (which, event) {
        if (!AJS.$("#hover").hasClass("mouseover")) {
            return false;//don't fire if the link is no longer hovered
        }

        var source = which.attr("name").split("-")[0];
        var subject = which.attr("name").split("-")[1];
        var content;
        var offset = which.offset();
        var height = which.height();

        if (source === "user") {
            content = hovers.users[subject];
        }
        else if (source === "item") {
            content = hovers.items[subject];
        }

        AJS.$("#hover-content").html(content);
        AJS.$("#hover").css({
            display:"block",
            top: offset.top + height,
            left: offset.left
        });
    };

    FECRU.UI.changesetToggle = function (postOpenFn) {
        return function() {
            AJS.$(document).delegate(".stream-delta dt.hasDiff", "click", function (e) {
                var target = e.target.tagName.toLowerCase();
                if (target == "a" || target == "span") {
                    return;
                }

                var $node = AJS.$(this);
                $node.next().toggle();
                $node.toggleClass("open");
                if (postOpenFn) {
                    postOpenFn(AJS.$(this));
                }
            });
        };
    };
    FECRU.UI.changesetToggleAll = function () {
        AJS.$("#expand-all").click(function() {
            AJS.$(".stream-delta dd").show();
            AJS.$(".delta-toggle").html("collapse all").addClass("expanded");
            AJS.$(".stream-delta dt.hasDiff").addClass("open");
        });
        AJS.$("#collapse-all").click(function() {
            AJS.$(".stream-delta dd").hide();
            AJS.$(".delta-toggle").html("expand all").removeClass("expanded");
            AJS.$(".stream-delta dt.hasDiff").removeClass("open");
        });
    };

    FECRU.UI.initStream = function () {
        linkAugment();
        formAugment();
    };

    FECRU.UI.contentPadBottom = function () {
        var contentPadding = AJS.$("#content").css("padding-bottom");
        var messageHeight = AJS.$("#footer-bar .system-message").height();
        var messagePadding = parseInt(contentPadding, 10) + messageHeight;
        AJS.$("#content").css("padding-bottom", messagePadding);
    };

    /**
     * checks if the dates in the elements ".calendar-date-end" and ".calendar-date-start" are in the correct order
     * and swap them around if they are found to be reversed.
     *
     * @param extractDateStringFn a function to extract the date string into the format 'yy-mm-dd' from the format
     * obtained in the value attribute of the input element
     */
    FECRU.UI.swapDatesIfReversed = function(extractDateStringFn, context) {
        context = context || "body";
        var endDateInput = AJS.$("input.calendar-date-end", context);
        var startDateInput = AJS.$("input.calendar-date-start", context);
        if (endDateInput.length > 0 && startDateInput.length > 0) {
            var startDateStr = startDateInput.val(),
                endDateStr = endDateInput.val(),
                extractedStartDate = extractDateStringFn(startDateStr),
                extractedEndDate = extractDateStringFn(endDateStr),
                startDate,
                endDate,
                errorThrown = false;

            try {
                startDate = AJS.$.datepicker.parseDate('yy-mm-dd', extractedStartDate);
            } catch (e) {
                FECRU.AJAX.appendErrorMessage("Could not parse start date " + extractedStartDate);
                errorThrown = true;
            }

            try {
                endDate = AJS.$.datepicker.parseDate('yy-mm-dd', extractedEndDate);
            } catch (e) {
                FECRU.AJAX.appendErrorMessage("Could not parse end date " + extractedEndDate);
                errorThrown = true;
            }

            if (errorThrown) {
                FECRU.AJAX.showErrorBox();
                return false;
            }

            if (startDateStr && endDateStr && (startDate > endDate)) {
                //do a swap of the date values before submitting if the dates are found to be reversed
                endDateInput.val(startDateStr);
                startDateInput.val(endDateStr);
            }
        }
        return true;
    };

    FECRU.UI.setupCalendar = function(addTime, constrainInput) {
        if (addTime === undefined || addTime === null) {
            addTime = true;
        }
        if (constrainInput === undefined || constrainInput === null) {
            constrainInput = true;
        }
        var calDateStart = AJS.$("input.calendar-date-start");
        calDateStart.attr("autocomplete", "off");
        calDateStart.datepicker({
            dateFormat: 'yy-mm-dd',
            constrainInput: constrainInput,
            onClose: function(dateText) {
                //only do this if the text is the length of a date
                //this will FAIL if anyone changes the date format

                //NOTE: yy-mm-dd in jquery is a 10 character format
                // ie 1987-12-23
                if (addTime && dateText.length === 10) {
                    AJS.$(this).attr("value", dateText + "T00:00:00");
                }
            }
        });
        var calDateEnd = AJS.$("input.calendar-date-end");
        calDateEnd.attr("autocomplete", "off");
        calDateEnd.datepicker({
            dateFormat: 'yy-mm-dd',
            constrainInput: constrainInput,
            onClose: function(dateText) {
                if (addTime && dateText.length === 10) {
                    AJS.$(this).attr("value", dateText + "T23:59:59");
                }
            }
        });
    };

    FECRU.UI.warnAboutFirebug = function (onClose) {
        var cookieName = 'hide_fecru_fb_warn';
        var suppressWarning = AJS.$.cookie(cookieName) === 'Y';
        if (!suppressWarning && window.console && window.console.firebug) {
            var product = AJS.$("#product-name").text() || "FishEye + Crucible";
            var $warning = AJS.$("<div id='firebug-warning'><p>Firebug is known to cause performance problems with " +
                                 product + ". Why not disable it?</p><a class='close'>X</a></div>");
            AJS.$(document).delegate("#firebug-warning .close", "click", function () {
                $warning.slideUp('fast', function() {
                    AJS.$.cookie(cookieName, 'Y', { expires: 365 });
                    if (onClose) {
                        onClose();
                    }
                });
            });
            $warning.prependTo(AJS.$("#masthead"));
        }
    };

    FECRU.UI.toggleSearch = function () {
        AJS.$(document).delegate("h5:[rel='toggle']", "click", function() {
            if (AJS.$(this).hasClass("show")) {
                AJS.$("#search-more").show();
                AJS.$(this).removeClass("show").addClass("hide");
                AJS.$(this).find("em").text("hide");
            }
            else {
                AJS.$("#search-more").hide();
                AJS.$(this).addClass("show").removeClass("hide");
                AJS.$(this).find("em").text("show");
            }
            return false;
        });
    };

    FECRU.UI.setCompletedResizeTimeout = function (selector, callback, completionDelay) {
        completionDelay = completionDelay || 100;
        var timeout;

        AJS.$(selector).resize(function () {
            if (completionDelay > 0 && timeout) {
                clearTimeout(timeout);
            }
            timeout = setTimeout(callback, completionDelay);
        });
    };
    
    FECRU.UI.setupRevisionPopups = function (selector) {
        if (!AJS.$.browser.msie) {
            AJS.$(document).delegate(selector + " tr .revision", "mouseover", function () {
                var $trigger = AJS.$(this).find(".revision-popup:first").css("visibility", "visible");
                // Hide any old drop downs if they aren't our own. Moving between rows doesn't close the old dropdown,
                // it only hides it.
                var current = AJS.dropDown.current;
                if (current && current.$.closest(".revision-popup")[0] !== $trigger[0]) {
                    current.hide();
                }
            }).delegate(selector + " tr .revision", "mouseout", function () {
                AJS.$(this).find(".revision-popup")
                        .css("visibility", "hidden");
            });
        } else {
            AJS.$(document).delegate(selector + " tr .revision", "mouseover", function () {
                if (!AJS.dropDown.current) {
                    AJS.$(this).find(".revision-popup:first").css("visibility", "visible");
                }
            }).delegate(selector + " tr .revision", "mouseout", function () {
                if (!AJS.dropDown.current) {
                    AJS.$(this).find(".revision-popup:first").css("visibility", "hidden");
                }
            });
        }
    };

    FECRU.UI.resizeAndCollateImagesToThumbs = function () {
        var $comments = AJS.$(".article-message .long-message.markup").filter(":has(.image-wrap)"),
            commentsSize = $comments.size(),
            thumbify = function ($img) {

                // Only thumb the image if it hasn't already been thumbed
                if ($img.parent().parent().hasClass('image-wrap')) {
                    var width = $img[0].width, // we use the dom element properties to ingore the fact the image isn't visible
                        height = $img[0].height,
                        thumbs = {
                            width: 100,
                            height: 67
                    };

                    if  (width > thumbs.width || height > thumbs.height) {
                        var wRatio = width / thumbs.width,
                            hRatio = height / thumbs.height;

                        if (wRatio > hRatio) {
                            $img.width(thumbs.width);
                        } else {
                            $img.height(thumbs.height);
                        }
                    }
                    // IE workaround. We want to ensure this method is run again when the long message becomes visible
                    if (width !== 0 || height !== 0) {
                        // Remove the class which hides the image now that we've thumbed it
                        $img.closest(".image-wrap").removeClass("image-wrap").addClass("image-wrapped");
                    }
                }
            };

        for (var i = 0; i < commentsSize; i++) {
            var $wrappers = $comments.eq(i).find(".image-wrap"),
                wrappersSize = $wrappers.size(),
                $img = $wrappers.children("img"),
                className = "comment-t" + i;

            // Unfortunately we have to process each image separately
            AJS.$.each($img, function() {
                var $this = AJS.$(this);
                $this.wrap('<a href="' + $img.attr('src') + '" class="fancybox-image" rel="' + className + '"></a>');
                $this.load(function(){
                    thumbify($this);
                });
                if (this.complete) {
                    $this.trigger("load");
                }
            });
        }

        // Fancy box all the images in the comment into a gallery
        AJS.$(".fancybox-image").fancybox({
            transitionIn : 'elastic',
            transitionOut : 'elastic'
        });
    };
})();
;
/* END /2static/script/fecru/ui.js */
/* START /2static/script/fecru/util.js */
window.FECRU = window.FECRU || {};

/**
 * jQuery uses [,] and : as special characters for selectors. These characters are still valid in HTML ids.
 * Use this method to return a sanitized version of the id for use in jQuery selectors.
 * @param id id to sanitize
 */
FECRU.sanitizeId = function(id) {
    return id.replace(/:/g, "\\:").replace(/\./g, "\\.");
};

/**
 * Pads a string with filler.  E.g.
 *      FECRU.pad(
 *          FECRU.pad(" Kwanzaa, ", 22, false, 'Merry, '),
 *          27, true, 'Joe')
 *  becomes "Merry, Merry Kwanzaa, JoeJo"
 * @param input - the input string to pad
 * @param length - the minimum length of the string
 * @param padRight - if true, add padding on the right side, otherwise add it to the left
 * @param padString - the string to pad the output with, default is a space.
 * @return a string padded to the requested length.
 */
FECRU.pad = function (input, length, padRight, padString) {
    padString = String(padString || ' ');
    var str = String(input);
    if (str.length < length) {
        var padLength = length - str.length,
            // padding is: (N repetitions of padString) + remaining substring of padString to fit.
                padding = (new Array(1 + Math.floor(padLength / padString.length)).join(padString)) +
                        padString.substring(0, padLength % padString.length);
        return padRight ? str + padding : padding + str;
    }
    return str;
};

/**
 * Format an number as a comma-separated int.
 *
 * Ex: formatInt(1000000) -> "1,000,000", formatInt(1000000.001) -> "1,000,000"
 */
FECRU.formatInt = function(num, localSeparator) {
    var sep = localSeparator || ",",
            s = String(Math.floor(num)), sf = "",
            length = s.length;

    for (var i = 0; i < length; i++) {
        sf = s.charAt(length - 1 - i) + sf;
        if (i != length - 1 && i % 3 == 2) {
            sf = sep + sf;
        }
    }

    return sf;
};

/**
 * Given a changeset id, return a display version
 * @param repoType string representation of the repository type (svn|git|hg|p4|cvs)
 * @param id the changeset id to prettify
 */
FECRU.getDisplayCsId = function (repoType, id) {
    if (!repoType || !id) {
        throw "getDisplayCsId requires repoType and id arguments";
    }

    switch (repoType.toLowerCase()) {
        case "hg":
            return id.length > 12 ? id.substr(0, 12) : id;

        case "git":
            // Same as abbreviated commit hash
            return id.length > 7 ? id.substr(0, 7) : id;

        default:
            return id;
    }
};

/**
 * given a string, return a string with the quotes "quoted" - e.g., abc'def --> abc\'def
 * so that the string is safe to be used as json.
 * @param str
 */
FECRU.quoteString = function(str) {
    return str.replace(/('|\\")/g, "\\$1");
};

FECRU.truncateText = function (str, options) {
    if (!str) {
        return str;
    }
    var defaults = {
        maxLength: 10,
        ellipsis: "..."
    };

    var opts = AJS.$.extend({}, defaults, options);

    if (opts.maxLength < 1 || str.length <= opts.maxLength) {
        return str;
    } else if (opts.maxLength == 1) {
        return str.substring(0, 1) + opts.ellipsis;
    }

    var midpoint = Math.ceil(str.length / 2),
        removeCount = str.length - opts.maxLength,
        lStrip = Math.ceil(removeCount / 2),
        rStrip = removeCount - lStrip;

    var truncated = str.substring(0, midpoint - lStrip) + opts.ellipsis + str.substring(midpoint + rStrip);
    // Let's not be idiotic about our "truncation" if we are generating a string of equal or greater length
    if (truncated.length >= str.length) {
        return str;
    }
    return truncated;
};

/**
 * Copied off ExpressionUtil.escapeHtmlIdOrName()
 */
FECRU.zEscape = (function() {
    var A = 'A'.charCodeAt(0),
        Z = 'Z'.charCodeAt(0),
        a = 'a'.charCodeAt(0),
        z = 'z'.charCodeAt(0),
        zero = '0'.charCodeAt(0),
        nine = '9'.charCodeAt(0);

    function isAsciiLetter(c) {
        return (A <= c && c <= Z) || (a <= c && c <= z);
    }

    function isAsciiDigit(c) {
        return (zero <= c && c <= nine);
    }

    function isAsciiLetterOrDigit(c) {
        return isAsciiDigit(c) || isAsciiLetter(c);
    }

    function toHexString(c) {
        return c.toString(16);
    }

    return function(s) {
        if (!s) {
            return null;
        }
        var out = [],
            hex = undefined,
            c = undefined;
        for (var i = 0, len = s.length; i < len; i++) {
            c = s.charCodeAt(i);
            //prepend 'z' if first letter is not a letter
            if ((i === 0 && !isAsciiLetter(c)) || c === Z) { //always escape 'Z' to prevent dupe ids
                out.push('Z');
            }
            if (isAsciiLetterOrDigit(c)) {
                out.push(String.fromCharCode(c));
            } else {
                switch (c) {
                    case '-'.charCodeAt(0):
                    case '_'.charCodeAt(0):
                    case '.'.charCodeAt(0):
                        out.push(String.fromCharCode(c));
                        break;
                    default:
                        //escape non-valid id characters & pad to 4 chars
                        out.push('Z');
                        hex = toHexString(c);
                        out.push(FECRU.pad("", 4 - hex.length, false, "0"));
                        out.push(hex);
                }
            }
        }
        return out.join("");
    };
})();

/**
 * NOTE: Avoid changing this implementation as the colors in the Commit Graph will be affected.
 * @param string A string to get the numerical hash for.
 * @param modulo A number that the hash will be mod'd by before being returned.
 * @return A number between zero (inclusive) and modulo (exclusive)
 */
FECRU.numericHash = function hash(string, modulo) {
    var hash = 0;
    for(var i = 0, l = string.length; i < l; i++) {
        hash = (hash * 31) + string.charCodeAt(i);
    }
    return hash % 100043 % modulo;
};

/**
 * Parse a date formatted as "YYYY-MM-DDTHH:mm:ss" and interpreted
 * in the user's local timezone.
 */
FECRU.parseISO8601Date = function(str) {
    var parts = str.split("T"),
        dateParts = parts[0].split("-"),
        timeParts = parts[1].split(":"),
        year = parseInt(dateParts[0], 10),
        month = parseInt(dateParts[1], 10) - 1, // Jan = 0
        day = parseInt(dateParts[2], 10),
        hour = parseInt(timeParts[0], 10),
        minute = parseInt(timeParts[1], 10),
        second = parseInt(timeParts[2], 10);

    return new Date(year, month, day, hour, minute, second);
};

/**
 * Given two dates in the user's local timezone,
 * calculates the number of working days between them.  A working day is currently defined
 * as 24 hours occurring on dates that are Monday-Friday. So Friday 1pm to Monday 1pm is exactly 1 working day.
 *
 * NOTE: Should mimic ISO8601DateHelper.java's method
 */
FECRU.getElapsedWorkingDays = function(startDate, endDate) {
    if (endDate < startDate) {
        return -FECRU.getElapsedWorkingDays(endDate, startDate);
    }

    var currDate = new Date(endDate);

    var workDays = 0;

    while(currDate >= startDate) {
        if (currDate.getDay() !== 6 && currDate.getDay() !== 0) {
            workDays++;
        }
        currDate.setDate(currDate.getDate() - 1);
    }

    // If the first day is a work day, we have counted it, even though no time is elapsed.
    // So we remove that 1 day here.  An alternative explanation would be that
    // this handles the case where startDate is a weekend.  This way the difference between
    // any time Saturday and any time Friday is 0 days, Friday 4pm to Monday 3pm is 0 days, and
    // Friday 4pm to Monday 5pm is 1 day.
    workDays = Math.max(workDays - 1, 0);

    return workDays;
 };

FECRU.pluralise = function(singular, plural, number) {
    return number === 1 || number === -1 ? singular : plural;
};;
/* END /2static/script/fecru/util.js */
/* START /2static/script/fecru/recently-visited.js */
window.FECRU = window.FECRU || {};
FECRU.RECENT = FECRU.RECENT || {};

(function($) {

    var fecruRecent = FECRU.RECENT;

    fecruRecent.dropDownPopulatedMap = {
        "source" : false,
        "projects" : false,
        "people" : false,
        "reviews" : false
    };

    // Tracks which menu dropdown is in the active state
    var activeDropdown = undefined;

    // Overriding AUIs cache object. Allows us to share a cache
    // between all the drop downs for performance win
    var sharedCache = (function () {
        var c = {};
        return {
            get: function (ajaxOptions) {
                return c[(ajaxOptions.url)];
            },
            set: function (ajaxOptions, responseData) {
                c[(ajaxOptions.url)] = responseData;
            },
            reset: function () {
                c = {};
            }
        };
    })();

    /*
    @param headerText The text to be used as the title of this section.
    @param itemsData An array of objects with linkUrl and linkText properties
    @returns A string containing the HTML representation of the section.
    */
    var sectionAsString = function(headerText, itemsData) {
        var $items = $("<div/>");
        for (var i = 0, l = itemsData.length; i< l; i++) {
            var item = itemsData[i];
            var MAX_LENGTH = 27; //the max length of text before truncation occurs
            var linkTextSuffix = item.linkTextSuffix ? item.linkTextSuffix : "";
            var fullLinkText = item.linkText + linkTextSuffix;
            var linkTextMax = MAX_LENGTH - linkTextSuffix.length - (item.iconUrl ? 4 : 0);
            var shortLinkText = item.linkText.substr(0, linkTextMax);

            var $a = $("<a />");
            $a.attr("href", item.linkUrl);
            if (shortLinkText != item.linkText) {
                shortLinkText += "...";
                $a.attr("title", fullLinkText);
            }
            if (item.iconUrl) {
                $a.css("background-image", "url('" + item.iconUrl + "?s=16')");
                $a.addClass("dd-icon");
            }
            shortLinkText += linkTextSuffix;
            $a.text(shortLinkText);
            $items.append($("<li/>").append($a));
        }
        return "<h5>" + headerText + "</h5><ol>" + $items.html() + "</ol>";
    };

    /*
    When called on an AUI dropdown object with section parameters, adds an HTML representation of those sections
    to the dropdown in the pentultimate position.

    @param sections... A variable number of arguments that are objects with the following structure:
                        {
                            headerText: "A Header"
                        ,   itemsData: [
                                {
                                    linkUrl: "http://www.example.com"
                                ,   linkText: "Happy Time WebSite FunPlace!"
                                ,   linkTextSuffix: "Not!"
                                ,   iconUrl: "/foo/user/avatar/"
                                }
                            ,   ...
                            ]
                        }
    */
    var populateDropdown = function(/* sections... */) {
        var $insertBeforeElement = this.$.children('ol:last');

        var sections = "";
        for (var i = 0, l = arguments.length; i < l; i++) {
            var sectionData = arguments[i];
            if (sectionData.itemsData && sectionData.itemsData.length &&
                    this.$.children('h5:contains("' + sectionData.headerText + '")').size() === 0) {
                sections += sectionAsString(sectionData.headerText, sectionData.itemsData) || "";
            }
        }

        $(sections).insertBefore($insertBeforeElement);
        this.$.closest(".menu-bar").addClass("menu-loaded");
        this.reset(); // Binds jquery events to new html
    };

    /*
       Create a recent items dropdown using the given options, the given callback function, and the shared cache.
       @param options The options object that gets passed to AUI
       @param onRefreshSuccess The function to call with a response string parameter when this control refreshes.
    */
    var initDropDown = function(options, onRefreshSuccess, isAjax) {
        var dropDowns;
        if (isAjax) {
            dropDowns = AJS.dropDown.Ajax($.extend(true, {}, options));
        } else {
            dropDowns = AJS.dropDown.Standard($.extend(true, {}, options));
        }

        var watched = false;

        $(dropDowns).each(function () {
            var ddcontrol = this;
            if (isAjax) {
                ddcontrol.cache = sharedCache;
                ddcontrol.refreshSuccess = onRefreshSuccess;
            }

            ddcontrol.addCallback("show", function () {
                if (!watched) {
                    watchForDropdownHover(options.selector, options.trigger);
                    watched = true;
                }
                activeDropdown = this;
            });
            ddcontrol.addCallback("hide", function () {
                if (activeDropdown === this) {
                    activeDropdown = undefined;
                }
            });
        });
    };

    // mark the dropdowns as erros if there is an error in the ajax call to fetch the list.
    var errorHandler = function() {
        AJS.$("#main-nav .aui-dd-parent").closest(".menu-bar").addClass("menu-loaded menu-error");
    };

    fecruRecent.setDropdowns = function() {
        var options = {
            selectionHandler: function () {
                // Prevent AUI from reimplementing default browser handling of <a> elements.
                // May need to revisit depending on AUI changes (see CR-FE-1617).
            },
            useLiveEvents: false, //Can't use live events with aui ajax dropdown
            selector: '#main-nav .aui-dd-parent', // Increase specificity as its not a live event
            trigger: '.aui-dd-link',
            alignment: "left",
            ajaxOptions: {
                dataType:"json",
                type: "GET",
                url: FECRU.pageContext + "/rest-service-fecru/recently-visited-v1/detailed.json",
                error : errorHandler
            }
        };

        var dropDownMapping = [{
            dropDownSelector: '.source-dropdown',
            populateFunction: function(response) {
                var currentRepositoryName = this.$.children("input[name=currentRepositoryName]").val();
                populateDropdown.apply(this, [{
                    headerText: "Recent Repositories",
                    itemsData: AJS.$.map(response["repositories"]["repository"], function(item) {
                        return currentRepositoryName == item.entityId ? null :  {
                            linkText: item.entityId,
                            linkUrl: FECRU.pageContext + item.uri
                        };
                    })
                }]);
                fecruRecent.dropDownPopulatedMap.source = true;
            }
        },
        {
            dropDownSelector: '.projects-dropdown',
            populateFunction: function(response) {
                var currentProjectKey = this.$.children("input[name=currentProjectKey]").val();
                populateDropdown.apply(this, [{
                    headerText: "Recent Projects",
                    itemsData: AJS.$.map(response["projects"]["project"], function(item) {
                        if (currentProjectKey == item.entityId) {
                            return null;
                        }
                        var project = item.projectData;
                        return  {
                            linkText: project ? project.name : item.entityId,
                            linkTextSuffix: project ? " (" + item.entityId + ")" : null,
                            linkUrl: FECRU.pageContext + item.uri
                        };
                    })
                }]);
                fecruRecent.dropDownPopulatedMap.projects = true;
            }
        },
        {
            dropDownSelector: '.people-dropdown',
            populateFunction: function(response) {
                populateDropdown.apply(this, [{
                    headerText: "Recent Users",
                    itemsData: AJS.$.map(response["users"]["user"], function(item) {
                        var user = item.userData;
                        return {
                            linkText: user ? user.displayName : item.entityId,
                            linkTextSuffix: user ? " (" + item.entityId + ")" : null,
                            linkUrl: FECRU.pageContext + item.uri,
                            iconUrl: user ? user.avatarUrl : null
                        };
                    })
                }]);
                fecruRecent.dropDownPopulatedMap.people = true;
            }
        },
        {
            dropDownSelector: '.reviews-dropdown',
            populateFunction: function(response) {
                populateDropdown.apply(this, [{
                    headerText: "Recent Reviews",
                    itemsData: AJS.$.map(response["reviews"]["review"], function(item) {
                        var review = item.reviewData;
                        var linkText = item.entityId;
                        if (review) {
                            linkText += ": " + review.name;
                        }
                        return {
                            linkText: linkText,
                            linkUrl: FECRU.pageContext + item.uri
                        };
                    })
                }, {
                    headerText: "Recent Snippets",
                    itemsData: AJS.$.map(response["snippets"]["snippet"], function(item) {
                        var snippet = item.snippetData;
                        var linkText = item.entityId;
                        if (snippet) {
                            linkText += ": " + snippet.name;
                        }
                        return {
                            linkText: linkText,
                            linkUrl: FECRU.pageContext + item.uri
                        };
                    })
                }]);
                fecruRecent.dropDownPopulatedMap.reviews = true;
            }
        }];

        for (var i = 0, l = dropDownMapping.length; i < l; i++) {
            options.dropDown = dropDownMapping[i].dropDownSelector;
            initDropDown(options, dropDownMapping[i].populateFunction, !FECRU.isAnon);
        }
    };

    /**
     * When entering into a dropdown target, hide the active one and show the target if there is a
     * dropdown which is already open.
     */
    var watchForDropdownHover = function (selector, trigger) {
        $(selector).bind("mouseenter", function () {
            if (activeDropdown) {
                activeDropdown.hide();
                $(this).find(trigger).click();
            }
        });
    };

})(AJS.$);
;
/* END /2static/script/fecru/recently-visited.js */
/* START /2static/script/fecru/onReady.js */
//init
AJS.toInit(function(){
    var ui = FECRU.UI;
    var recent = FECRU.RECENT;

    ui.initStream();

    ui.setDropdowns();
    recent.setDropdowns(); // Recently Visitied Ajax Dropdowns

    FECRU.HOVER.addAllLinkPopups();

    FECRU.RSS.setupRSSDialog();

    ui.accordion();
});
;
/* END /2static/script/fecru/onReady.js */
/* START /2static/script/fecru/search.js */
var FECRU = FECRU || {};

FECRU.SEARCH = (function($){

    var submit = function () {
        var $repository = $("#repository-input");

        if ($repository.val() === FECRU.SEARCH.placeholder) {
            $repository.val("");
        }

        $("#search-form").submit();
    },
    init = function () {
        $("#search-button").click(function () {
            submit();
        });

        setupPathAbbreviation();

        // format counters
        $("#content-column .toolbar .count, .search-sidebar-types .active .count").each(function() {
            var $counter = $(this);
            $counter.text(FECRU.formatInt($counter.text()));
        });

        // ensure that the search box starts with the focus
        $("#search-input").focus();

    },
    setupPathAbbreviation = function() {
        var $paths = $(".file-path", $("#search-results")),
            $repoNames = $paths.parent().siblings('.repo-name'),
            maxRepoNameWidth = 0;
        for (var i = 0, l = $repoNames.length; i < l; i++) {
            maxRepoNameWidth = Math.max(maxRepoNameWidth, $repoNames.eq(i).width());
        }
        var triggerPathAbbreviation = function(hint) {
            $paths.abbreviatePath({
                boundingWidthGetter : function($this) {
                    return $this.closest('li').width() - maxRepoNameWidth;
                },
                growingElementGetter : function($this) { return $this.parent() /*span.abbreviate-path-grower*/; },
                directionHint : hint,
                resetBoundsWidth : true
            });
        };

        //initial truncation
        triggerPathAbbreviation(null);

        //on resize
        var $window = $(window),
            windowWidth = $window.width();
        FECRU.UI.setCompletedResizeTimeout($window, function() {
            var newWindowWidth = $window.width(),
                shouldRun = windowWidth !== newWindowWidth,
                hint = windowWidth < newWindowWidth ? 'larger' : 'smaller';
            windowWidth = newWindowWidth;
            if (shouldRun) {
                triggerPathAbbreviation(hint);
            }
        }, 50);
    },
    lazyLoad = function (searchUrl, searchParameters) {
        // fire-off AJAX to get counts
        $.getJSON(searchUrl + '?' + searchParameters + '&countsOnly=true', function(data) {

            var showTypeCounts = false, total = 0;
            for (var type in data) {
                if (data.hasOwnProperty(type)) {

                    var count = data[type];

                    // update the side counters
                    var $countType = $("#count-" + type);
                    $countType.children("em").text(FECRU.formatInt(count)).show();
                    $countType.fadeIn(250);
                    total += count;

                    // if we have some search results for the current category
                    if (count > 0) {
                        $countType.parents("li.result-0").removeClass("result-0");

                        // update the counters in the message shown when the current category has no results:
                        // "No results here but n results available there"
                        $("#typecount-" + type).show().children("em").text(FECRU.formatInt(count)); // will appear only if showTypeCounts is true (below)
                        showTypeCounts = true;
                    }
                }
            }
            if (showTypeCounts) {
                $("#typecounts").fadeIn(250);
            }

            // update the total count of the results
            var $totalCount = $("#total-count");
            total += parseInt($totalCount.text());
            $totalCount.text(FECRU.formatInt(total)).fadeIn(250);
        });        
    },
    createChangeItemFunction = function (urlGetter) {
        return function(data) {
            if (data.disabled) {
                return;
            }

            var item = (data.id === FECRU.SEARCH.placeholder) ? "" : data.id,
                query = AJS.$("#search-input").val(),
                type = AJS.$("#search-type").val();

            document.location = urlGetter(item, query, type);
        };
    },
    changeRepository = createChangeItemFunction(function(repository, query, type) {
        return FECRU.pageContext + "/qsearch/" + encodeURI(repository) + "?q=" + encodeURI(query) + "&t=" + encodeURI(type);
    }),
    changeProject = createChangeItemFunction(function(project, query, type) {
        return FECRU.pageContext + "/qsearch?project=" + encodeURI(project) + "&q=" + encodeURI(query) + "&t=" + encodeURI(type);
    });


    return {
        changeRepository : changeRepository,
        changeProject : changeProject,
        submit : submit,
        init : init,
        lazyLoad : lazyLoad
    };
})(AJS.$);;
/* END /2static/script/fecru/search.js */
/* START /2static/script/fecru/star.js */
window.FECRU = window.FECRU || {};

(function() {

    /**
     * html for the star edit dialog, to be inserted on demand.
     */
    var DIALOG_DEFAULT = "Describe your favourite";
    var DIALOG_HTML = ["<div id='astronomy'>",
                          "<div id='astronomy-label'>",
                              "<h4><span>Update favourite</span></h4>",
                              "<span class='close'><a href='#' class='close-astronomy' id='close-astronomy'>X</a></span>",
                              "<form action='#'>",
                                  "<fieldset class='input'>",
                                      "<label for='star-labels'>Name</label>",
                                      "<input type='text' id='star-labels' value='Describe your favourite'>",
                                  "</fieldset>",
                                  "<fieldset class='button'>",
                                      "<ul>",
                                          "<li><button class='remove' id='remove-astronomy'>Remove</button></li>",
                                          "<li class='odd'><input type='submit' id='save-star-label' value='Save label'></li>",
                                      "</ul>",
                                  "</fieldset>",
                              "</form>",
                          "</div>",
                      "</div>"
                    ].join("");

    ////// private functions

    var ON_ONLY = true;
    var OFF_ONLY = false;

    function starOnOffClassName(on) {
        return on ? "star-on" : "star-off";
    }

    function starAddRemoveText($star, on) {
        return $star.find(on ? "input.star-textRemove" : "input.star-textAdd").val();
    }

    /**
     * todo: see if this can be swapped with the throbber jquery plugin.
     */
    function makeThrobberControl($link, throbberSetting) {
        /**
         * Starts the throbber, which activates after noLatencyThreshold milliseconds have passed.
         * @return a function that will stop the throbber after minThrobberDisplay milliseconds have passed, ensuring no flicker.
         */
        var startThrob = function () {
            $link.data("throbbing", true);
            var timeout = setTimeout(function () {
                $link.addClass("star-throb");
            }, throbberSetting.noLatencyThreshold);

            return function() {
                $link.data("throbbing", false);
                clearTimeout(timeout);
                setTimeout(function () {
                    $link.removeClass("star-throb");
                }, throbberSetting.minThrobberDisplay);
            };
        };

        /**
         * stores a function to stop the throbber for the given $link
         */
        var stopThrob = undefined;
        return {
            start: function() {
                if (throbberSetting.showThrob && !stopThrob) {
                    stopThrob = startThrob();
                }
            },
            stop: function() {
                if (stopThrob) {
                    stopThrob();
                    stopThrob = undefined;
                }
            },
            isThrobbing: function() {
                return $link.data("throbbing");
            }
        };
    }

    /**
     *
     * @param link the jquery element of the link that was clicked
     * @param dialogControl an object to control the dialog. Its members should be:
     * - showDialog: a function with no arguments, which when invoked will display the dialog
     * - hideDialog: a function with no arguments, which when invoked will hide the dialog
     * - getDialog: a function with no arguments, which returns a jquery object that represents the dialog
     * @param options the list of options. Available options are:
     * - throbber : options for throbbing
     *
     */
    function starClicked(link, dialogControl, options) {
        var starId = getStarId(link);
        var starKeys = {};
        var $link = AJS.$(link);
        var throbberControl = makeThrobberControl($link, options.throbberSetting);
        //if throbbing already, then don't do anything.
        if (throbberControl.isThrobbing()) {
            return;
        }

        $link.children("span.inputs").children("input.starKey").each(function() {
            var key = AJS.$(this);
            starKeys[key.attr("name")] = key.val();
        });
        if (starId != null) {
            if (isDialogShown($link)) {
                // we are unstarring, pop up the edit box
                editStar(starId, dialogControl, throbberControl);
            } else {
                //...or remove directly without anymore interaction
                doRemoveStar(starId, throbberControl);
            }
        } else {
            // we are adding a star
            addStar(starKeys, dialogControl, throbberControl);
        }

        // todo: fix this hack to make hoverpopups refresh after starring
        // just always invalidate all popups
        var fecruHover = FECRU.HOVER;
        fecruHover.invalidateCache(fecruHover.CACHE_FOREVER);

    }

    function updateAccordian() {
        var $starList = AJS.$("#accordionStarList");
        if ($starList.length > 0) {
            FECRU.AJAX.ajaxDo(FECRU.pageContext + "/json/profile/starAccordionAjaxBody.do", {},
                    function(resp) {
                        if (resp.worked) {
                            $starList.replaceWith(resp.html);
                        }
                    },
                    false
                    );
        }
        var $numStars = AJS.$("#accordionNumStars");
        if ($numStars.length > 0) {
            FECRU.AJAX.ajaxDo(FECRU.pageContext + "/json/profile/starAccordionAjaxNumber.do", {},
                    function(resp) {
                        if (resp.worked) {
                            $numStars.replaceWith(resp.html);
                        }
                    },
                    false
                    );
        }
    }

    function getIdElement(link) {
        return AJS.$(link).children("span.inputs").children("input[name='id']");
    }

    function getStarId(link) {
        return getIdElement(link).val();
    }

    function allStars(on) {
        return AJS.$("a." + starOnOffClassName(on));
    }

    function setStarAttributes(on, $node) {
        var newClass = starOnOffClassName(on);
        var oldClass = starOnOffClassName(!on);
        $node.removeClass(oldClass)
             .addClass(newClass)
             .children("span.starText").text(starAddRemoveText($node,on));
    }

    /**
     * Turn on the Star with the given keys
     * @param keys
     */
    function addStar(keys, dialogControl, throbberControl) {
        var paramMap = {};
        for (var key in keys) {
            paramMap["key." + key] = keys[key];
        }
        throbberControl.start();
        doStar(FECRU.pageContext + "/json/profile/addStarAjax.do", paramMap, function(newId) {
            allStars(OFF_ONLY).each(function() {
                var $node = AJS.$(this);
                var $inputs = $node.children("span.inputs");
                for (var key in keys) {
                    var input = $inputs.children("input.starKey[name='" + key + "']");
                    if (input.length === 0 || input.val() != keys[key]) {
                        return;
                    }
                }
                setStarAttributes(true, $node);
                $inputs.append("<input type='hidden' name='id' value='" + newId + "'>");
            });
            var itemType = keys["itemType"];
            if (itemType == "atlassian-chart" || itemType == "atlassian-search" ||
                itemType == "atlassian-quicksearch") {
                editStar(newId, dialogControl, throbberControl);
            } else {
                // WARNING: epic hack
                // This is to work around an incorrect usage of InlineDialog. We are leveraging IDs triggers to load
                // star ajax dialog, but in some cases we are not actually showing the dialog. This leaves the internal
                // state of InlineDialog in tatters and further calls to show the dialogs won't work. This workaround
                // simply shows and immediately hides the dialog ensuring the internal state is correct. The hidden
                // visibility is to ensure that the browser doesn't render the dialog on the screen.
                var $dialog = AJS.$("#inline-dialog-star-inline-dialog");
                $dialog.css("visibility", "hidden");
                dialogControl.showDialog();
                dialogControl.hideDialog();
                $dialog.css("visibility", "visible");
            }
            throbberControl.stop();
        });
    }

    /**
     * Pop up a dialog box for a Star which is in the on state, allowing the user to set its label or to remove it.
     * @param starId the id of the Star to change.
     * @param dialogControl
     */
    function editStar(starId, dialogControl, throbberControl) {
        // pop up our star edit dialog
        var onSuccess = function(resp) {
            if (resp.worked) {
                var __updateLabel = function(label) {
                    doSaveLabel(starId, label, throbberControl);
                };
                var __removeStar = function() {
                    doRemoveStar(starId, throbberControl);
                };
                showStarDialog(dialogControl, __updateLabel, __removeStar, resp.payload);
            } else {
                //resp.worked is false, reset the star to an off star.
                turnOffStar(starId);
            }
            throbberControl.stop();
        };
        throbberControl.start();
        // get the label for this Star, to supply it to the dialog
        AJS.$.post(FECRU.pageContext + "/json/profile/getStarLabelAjax.do", {id: starId}, onSuccess, 'json');
    }

    function turnOffStar(starId) {
        allStars(ON_ONLY).each(function() {
            var id = getStarId(this);
            if (starId == id) {
                // represents the same star
                getIdElement(this).remove();
                var $node = AJS.$(this);
                setStarAttributes(false, $node);
            }
        });
    }

    /**
     * Send a new label value to the server.
     * @param id the id of the Star who's label has been changed
     * @param label a String holding the new label text
     */
    function doSaveLabel(id, label, throbberControl) {
        var onDone = function() {
            refreshDropDown();
            throbberControl.stop();
        };
        throbberControl.start();
        AJS.$.post(FECRU.pageContext + "/json/profile/setStarLabelAjax.do", {id:id, label:label}, onDone, 'json');
    }

    /**
     * Unstar the star with the given id.
     *
     * @param id the id of the Star to unstar.
     */
    function doRemoveStar(id, throbberControl) {
        throbberControl.start();
        doStar(FECRU.pageContext + "/json/profile/removeStarAjax.do", {id: id}, function() {
            turnOffStar(id);
            throbberControl.stop();
        });
    }

    /**
     * Change the state of a set of stars, and refresh the 'my stars' dropdown.
     * @param url the URL to inform the server about the change
     * @param updateStars the function to call to change the state of the stars in the browser
     */
    function doStar(url, params, updateStars) {
        var done = function(resp) {
            if (resp.worked) {
                updateStars(resp.id);
                refreshDropDown();
                updateAccordian();
                return true;
            }
        };
        FECRU.AJAX.ajaxDo(url, params, done, false);
    }

    function refreshDropDown() {
        var $dropDown = AJS.$("#starDropDownList");
        if ($dropDown.length > 0) {
            var done = function(resp) {
                if (resp.worked) {
                    $dropDown.replaceWith(resp.html);
                }
            };
            FECRU.AJAX.ajaxDo(FECRU.pageContext + "/json/profile/starDropDownAjax.do", {}, done, false);
        }
    }

    //todo: use AUI inline dialog
    function createStarDialog(dialogHider) {
        var $dialog = AJS.$(DIALOG_HTML);
        //intialize the dialog buttons
        AJS.$("#close-astronomy", $dialog).click(function(e) {
            dialogHider && dialogHider();
            e.preventDefault();
        });

        var $starLabel = AJS.$("#star-labels", $dialog);

        $starLabel.keypress(function(e) {
            if ((e.which || e.keyCode) === AJS.$.ui.keyCode.ENTER) {// if return is clicked
                e.preventDefault();                
                AJS.$("#save-star-label", $dialog).trigger('click');
            }
        }).attr("autocomplete", "off");

        return $dialog;
    }

    function showStarDialog(dialogController, updateLabel, removeStar, currentLabel) {
        var $dialog = dialogController.getDialog();
        var $starLabel = AJS.$("#star-labels", $dialog);

        if(currentLabel) {
            $starLabel.attr("value", currentLabel).data("defaultValue", currentLabel).addClass("focussed");
        } else {
            $starLabel.data("defaultValue", $starLabel.val());
        }

        AJS.$("#remove-astronomy", $dialog).unbind().click(function(e) {
            e.preventDefault();
            removeStar();
            dialogController.hideDialog();
            $starLabel.val(DIALOG_DEFAULT).removeClass("focussed");            
        });

        AJS.$("#save-star-label", $dialog).unbind().click(function(e) {
            e.preventDefault();
            var newLabel = $starLabel.val();
            if (newLabel !== $starLabel.data("defaultValue")) {
                $starLabel.data("defaultValue", newLabel);
                updateLabel(newLabel);
            }
            dialogController.hideDialog();
        });
        dialogController.showDialog();
    }

    function isDialogShown(starLink) {
        return starLink.hasClass("showDialog");
    }

    /**
     * bind a live click event to the stars.
     */
    function bindStars() {
        var $dialog = undefined; //stores a reference to the jquery elmem
        var dialogHider = undefined;
        var PADDING = 5;
        var hoverOptions = {
            onHover: false,
            showArrow: true,
            fadeTime: 200,
            hideDelay: null,
            showDelay: 0,
            width: 240 + PADDING,
            offsetX: -8, //to get the arrow centred at the star's vertical asix
            offsetY: 4,
            container: "body",
            useLiveEvents: true,
            cacheContent:false,
            initCallback: function() {
                //implementation note: we need a way to hide the dialog on demand - this callback has a hook into
                //the hide function that inline-dialog uses internally, so this is saving a reference to that function
                //for use later.
                var that = this;
                dialogHider = function() {
                    that.hide();
                };
            }
        };
        var onStarClickedHandler = function ($contentDiv, trigger, showPopup) {
            var $link = AJS.$(trigger);
            var dialogControl = {
                showDialog: function() {
                    if (isDialogShown($link)) {
                        if ($dialog === undefined) {
                            $dialog = createStarDialog(dialogHider);
                        }
                        $dialog.appendTo($contentDiv).show();
                        var $starLabels = AJS.$("#star-labels");
                        $starLabels.placeholder($starLabels.val());
                        showPopup();
                    }
                },
                hideDialog: function() {
                    dialogHider && dialogHider();
                },
                getDialog: function () {
                    if ($dialog === undefined) {
                        $dialog = createStarDialog(this.hideDialog);
                    }
                    return $dialog;
                }
            };
            var opts = {
                throbberSetting: {
                    showThrob: true,         //todo: customizable throbbing needed?
                    noLatencyThreshold: 150, //if the ajax call takes less than this milliseconds, then no throb shown
                    minThrobberDisplay: 200  //otherwise, show for at least this milliseconds.
                }
            };
            starClicked(trigger, dialogControl, opts);
        };
        AJS.InlineDialog(".starrable.showDialog", "star-inline-dialog", onStarClickedHandler, hoverOptions);
        AJS.$(document).delegate('.starrable:not(.showDialog)', 'click', function() {
            onStarClickedHandler(null, this, null);
        });
    }

    ////// onload events for stars
    AJS.toInit(function() {
        bindStars();
    });
})();
;
/* END /2static/script/fecru/star.js */
/* START /2static/script/fecru/applinks.js */
window.FECRU = window.FECRU || {};
FECRU.UAL = FECRU.UAL || {};

(function($, namespace, window) {

    var ignoreCookieName = "ignoredAppLinks";

    // Applinks impl for doing OAuth authorizations.  Used by FECRU.AJAX
    var oAuthAuthorizationImpl = (function() {
        function reset() {
            if (window.aouthWindow) {
                window.aouthWindow.close();
            }

            window.oauthCallback = null;
            window.aouthWindow = null;
        }

        function createOAuthCallbackObj(authorizationUri, success, failure) {
            return {
                success : function() {
                    reset();

                    if (success) {
                        success();
                    }
                },
                failure : function() {
                    reset();

                    if (failure) {
                        failure();
                    }
                },
                uri : authorizationUri
            };
        }

        return { // injectable core for mocking
            authorize : function(authorizationUri, success, failure) {
                window.oauthCallback = createOAuthCallbackObj(authorizationUri, success, failure);
                window.aouthWindow = window.open(authorizationUri);
            },
            cancelAuthorization : function(authorizationUri) {
                if (window.oauthCallback && window.oauthCallback.uri === authorizationUri) {
                    reset();
                }
            }
        };
    })();

    function ignoreAppLink(applicationId) {
        var cookieValue = AJS.$.cookie(ignoreCookieName);
        if (!cookieValue) {
            cookieValue = "";
        }

        // set cookie so that anonymous users can at least have error messages suppressed
        cookieValue += (cookieValue.length > 0 ? "," : "") + applicationId;
        AJS.$.cookie(ignoreCookieName, cookieValue, { expires: 365, path: FECRU.pageContext });

        // also set server ignore preference for this applink
        var url = CRU.UTIL.jsonUrlBase() + "/ignoreLinkedApp.do";
        FECRU.AJAX.ajaxDo(url, {appLinkId : applicationId});
    }

    function clearIgnoredAppLinks(onComplete) {
        // clear cookie
        AJS.$.cookie("ignoredAppLinks", null, { path: FECRU.pageContext });

        var url = CRU.UTIL.jsonUrlBase() + "/clearIgnoreFlags.do";
        FECRU.AJAX.ajaxDo(url, {}, function() {
            if (AJS.$.isFunction(onComplete)) {
                onComplete();
            }
        });
    }

    /**
     * Remote exception notifier - display an error message, or prompt the user for authentication with a linked application
     *
     * @method warnRemoteActivityStream
     * @namespace AJS
     * @for AJS
     * @param applicationId {string} the ApplicationId of the remote application
     * @param message {string} a message to display
     * @param authUrl {string} the authorisation url to redirect the user to, if unspecified this method will display the
     * supplied message as an error message
     */
    function warnRemoteActivityStream(applicationId, applicationName, message, authUrl) {


        var cookieValue = AJS.$.cookie(ignoreCookieName);
        var suppressWarning = false;
        if (cookieValue) {
            suppressWarning = cookieValue.indexOf(applicationId) > -1;
        }

        if (authUrl) {
            if (!suppressWarning) {
                var linkId = "provide-credentials-" + applicationId;
                var $warning = AJS.$("<div class='remote-credentials-required-warning stream-notice'><p>" + message + "</p>" +
                                     "<a id='" + linkId + "'>Authenticate</a> or <a class='ual-ignore-applink'>ignore authentication requests from " + applicationName + "</a></div>");
                AJS.$(".ual-ignore-applink", $warning).click(function () {
                    FECRU.HOVER.invalidateCache(FECRU.HOVER.CACHE_FOREVER)
                    $warning.slideUp("fast");
                    ignoreAppLink(applicationId);
                });
                AJS.$("#" + linkId, $warning).click(function () {
                    FECRU.OAUTH.authorize(authUrl, oAuthAuthorizationImpl).authorized(function() {
                        window.location.reload();
                    });
                });
                $warning.prependTo(AJS.$("#stream-notice-box"));
            }
        } else {
            if (!suppressWarning) {
                var $error = AJS.$("<div class='remote-error-warning stream-error'><p>" + message + "</p>" +
                                         "<a class='ual-ignore-applink'>Ignore errors from " + applicationName + "</a></div>");
                AJS.$(".ual-ignore-applink", $error).click(function () {
                    $error.slideUp("fast");
                    ignoreAppLink(applicationId);
                });
                $error.prependTo(AJS.$("#stream-notice-box"));
            }
        }
    }


    function invalidateCache(issueKey) {
        FECRU.HOVER.invalidateCache(FECRU.HOVER.CACHE_FOREVER, issueKey);
    }

    AJS.$(document).delegate(".issue-hover-ignore-applink", "click", function() {
        var $this = AJS.$(this);
        var appLinkId = $this.attr('data-id');
        ignoreAppLink(appLinkId);
        var issueKey = $this.parents('.jirahover-details').attr('data-key');
        invalidateCache(issueKey);
        AJS.$("#inline-dialog-jira-hover-popup").hide();
    });

    AJS.$(document).delegate(".ual-authenticate", "click", function(e) {
        var $this = AJS.$(this);
        var issueKey = $this.parents('.jirahover-details').attr('data-key');
        invalidateCache(issueKey);
        AJS.$("#inline-dialog-jira-hover-popup").hide();

        FECRU.OAUTH.authorize($(this).attr("href"));

        e.preventDefault();
    });

    $.extend(namespace, {
        ignoreAppLink : ignoreAppLink,
        clearIgnoredAppLinks : clearIgnoredAppLinks,
        warnRemoteActivityStream : warnRemoteActivityStream,
        oAuthAuthorization : oAuthAuthorizationImpl
    });

})(AJS.$, FECRU.UAL, this);
;
/* END /2static/script/fecru/applinks.js */
/* START /2static/script/fecru/oauth.js */
window.FECRU = window.FECRU || {};

FECRU.OAUTH = (function($, window) {

    var Map = FECRU.DATA_STRUCTURES.Map,
        EventProducer = FECRU.MIXINS.EventProducer,
        fecruAjax = FECRU.AJAX,
        applinksAuthorizationImpl = FECRU.UAL.oAuthAuthorization;


    var oAuthAuthorizationInfos = new Map(),
        getOrCreateOauthAuthInfo = function(key) {
            var authInfo = oAuthAuthorizationInfos.get(key);
            if (!authInfo) {
                oAuthAuthorizationInfos.set(key, authInfo = new EventProducer(['authorized', 'authorizationFailed']));
            }
            return authInfo;
        },
        currentlyAuthenticating;
    function oAuthAuthorize(authorizationUri, optionalOAuthAuthorizationImpl) {
        var authInfo = getOrCreateOauthAuthInfo(authorizationUri);

        // Applinks doesn't support concurrent authentication (or rather, you can do it, but we can only get a callback for one at a time).
        // Therefore, "cancel" the last attempt and alert the user.
        if (currentlyAuthenticating && currentlyAuthenticating != authInfo) {
            currentlyAuthenticating.authorizationImpl.cancelAuthorization(currentlyAuthenticating.uri);
            currentlyAuthenticating.authorizationFailed();

            fecruAjax.appendNotificationMessage("To process your new authentication, FishEye disconnected from your previous authentication window. You" +
                " may need to reload the page before your remote authentication is registered.");
            fecruAjax.showNotificationBox("Authentication Cancelled", "message");
        }
        currentlyAuthenticating = authInfo;

        var authImpl = authInfo.authorizationImpl = optionalOAuthAuthorizationImpl || applinksAuthorizationImpl;
        authInfo.uri = authorizationUri;
        authImpl.authorize(authorizationUri, function() {
                if (currentlyAuthenticating === authInfo) {
                    currentlyAuthenticating = null;
                }
                authInfo.authorized();
            },
            function() {
                if (currentlyAuthenticating === authInfo) {
                    currentlyAuthenticating = null;
                }
                authInfo.authorizationFailed();
            }
        );

        return authInfo.getBinder();
    }
    function getOAuthAuthorizationEventProducer(authorizationUri) {
        return getOrCreateOauthAuthInfo(authorizationUri).getBinder();
    }
    function clearAllOAuthEventProducers() {
        oAuthAuthorizationInfos = new Map();
    }

    return {
        authorize : oAuthAuthorize,
        getEventProducer : getOAuthAuthorizationEventProducer,
        internalClearAllEventProducers : clearAllOAuthEventProducers
    };
})(AJS.$, this);;
/* END /2static/script/fecru/oauth.js */
/* START /2static/script/fecru/prefs.js */
window.FECRU = window.FECRU || {};
FECRU.PREFS = FECRU.PREFS || {};


FECRU.PREFS.setPreference = function(name, value, callback) {
    var params = {};
    params[name] = value;
    FECRU.PREFS.setPreferences(params, callback);
};

FECRU.PREFS.setPreferences = function(prefs, callback) {
    var params = {};
    for (i in prefs) {
        params["@" + i] = prefs[i];
    }
    FECRU.AJAX.ajaxDo(FECRU.pageContext + "/json/fe/setPreference.do", params, callback, false);
};

FECRU.PREFS.setupBinaryPrefLinks = function(name, cookieName, initialState, fns, onStateString, offStateString, forceInitialCall, reload, optionsSelector) {
    var onSelector = "." + name + onStateString;
    var offSelector = "." + name + offStateString;

    var setPref = function (value, hideSelector, showSelector, callback) {
        FECRU.PREFS.setPreference(cookieName, value, callback);
        setVisibility(value, hideSelector, showSelector, true);
    };

    var setVisibility = function (value, hideSelector, showSelector, callfunction) {
        var $optionsSelector = AJS.$(optionsSelector);
        $optionsSelector.find(hideSelector).css("display","none");
        $optionsSelector.find(showSelector).css("display","block");
        if(callfunction || forceInitialCall) {
           fns[value].call();
        }
    };

    var reloadPage = function () {
        if(reload) {
           window.location.reload();
        }
    };

    // make sure we extend the selector to every match, by splitting on ",
    var createSelector = function (base, extra) {
        var arr = base.split(",");
        for (var i = 0; i < arr.length; i++) {
            arr[i] = arr[i] + " " + extra;
        }
        return arr.join(",");
    };

    AJS.$(document).delegate(createSelector(optionsSelector, onSelector), "click", function() {
        setPref(onStateString, onSelector, offSelector, reloadPage);
        return false;
    }).delegate(createSelector(optionsSelector, offSelector), "click", function() {
        setPref(offStateString, offSelector, onSelector, reloadPage);
        return false;
    });
    if (initialState) {
        setVisibility(onStateString, onSelector, offSelector, false);
    } else {
        setVisibility(offStateString, offSelector, onSelector, false);
    }
};
;
/* END /2static/script/fecru/prefs.js */
/* START /2static/script/fecru/managed.js */
(function($) {
    // auto-select URL
    $(document).delegate(".vcsUrl", "click", function() {
        this.select();
    });
})(AJS.$);;
/* END /2static/script/fecru/managed.js */
/* START /2static/script/lib/graphael/g.raphael.js */
/*
 * g.Raphael 0.2 - Charting library, based on Raphaël
 *
 * Copyright (c) 2009 Dmitry Baranovskiy (http://g.raphaeljs.com)
 * Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license.
 */


(function () {
    Raphael.fn.g = Raphael.fn.g || {};
    Raphael.fn.g.markers = {
        disc: "disc",
        o: "disc",
        square: "square",
        s: "square",
        triangle: "triangle",
        t: "triangle",
        star: "star",
        "*": "star",
        cross: "cross",
        x: "cross",
        plus: "plus",
        "+": "plus",
        arrow: "arrow",
        "->": "arrow"
    };
    Raphael.fn.g.txtattr = {font: "12px Arial, sans-serif"};
    Raphael.fn.g.colors = [];
    var hues = [.6, .2, .05, .1333, .75, 0];
    for (var i = 0; i < 10; i++) {
        if (i < hues.length) {
            Raphael.fn.g.colors.push("hsb(" + hues[i] + ", .75, .75)");
        } else {
            Raphael.fn.g.colors.push("hsb(" + hues[i - hues.length] + ", 1, .5)");
        }
    }
    Raphael.fn.g.text = function (x, y, text) {
        return this.text(x, y, text).attr(this.g.txtattr);
    };
    Raphael.fn.g.labelise = function (label, val, total) {
        if (label) {
            return (label + "").replace(/(##+(?:\.#+)?)|(%%+(?:\.%+)?)/g, function (all, value, percent) {
                if (value) {
                    return (+val).toFixed(value.replace(/^#+\.?/g, "").length);
                }
                if (percent) {
                    return (val * 100 / total).toFixed(percent.replace(/^%+\.?/g, "").length) + "%";
                }
            });
        } else {
            return (+val).toFixed(0);
        }
    };

    Raphael.fn.g.finger = function (x, y, width, height, dir, ending, isPath) {
        // dir 0 for horisontal and 1 for vertical
        if ((dir && !height) || (!dir && !width)) {
            return isPath ? "" : this.path({});
        }
        ending = {square: "square", sharp: "sharp", soft: "soft"}[ending] || "round";
        var path;
        height = Math.round(height);
        width = Math.round(width);
        x = Math.round(x);
        y = Math.round(y);
        switch (ending) {
            case "round":
                if (!dir) {
                    var r = Math.floor(height / 2);
                    if (width < r) {
                        r = width;
                        path = ["M", x + .5, y + .5 - Math.floor(height / 2), "l", 0, 0, "a", r, Math.floor(height / 2), 0, 0, 1, 0, height, "l", 0, 0, "z"];
                    } else {
                        path = ["M", x + .5, y + .5 - r, "l", width - r, 0, "a", r, r, 0, 1, 1, 0, height, "l", r - width, 0, "z"];
                    }
                } else {
                    var r = Math.floor(width / 2);
                    if (height < r) {
                        r = height;
                        path = ["M", x - Math.floor(width / 2), y, "l", 0, 0, "a", Math.floor(width / 2), r, 0, 0, 1, width, 0, "l", 0, 0, "z"];
                    } else {
                        path = ["M", x - r, y, "l", 0, r - height, "a", r, r, 0, 1, 1, width, 0, "l", 0, height - r, "z"];
                    }
                }
                break;
            case "sharp":
                if (!dir) {
                    var half = Math.floor(height / 2);
                    path = ["M", x + .5, y + .5 + half, "l", 0, -height, Math.max(width - half, 0), 0, Math.min(half, width), half, -Math.min(half, width), half + (half * 2 < height), "z"];
                } else {
                    var half = Math.floor(width / 2);
                    path = ["M", x + half, y, "l", -width, 0, 0, -Math.max(height - half, 0), half, -Math.min(half, height), half + (half * 2 < height), Math.min(half, height), half, "z"];
                }
                break;
            case "square":
                if (!dir) {
                    path = ["M", x + .5, y + .5 + Math.floor(height / 2), "l", 0, -height, width, 0, 0, height, "z"];
                } else {
                    path = ["M", x + Math.floor(width / 2), y, "l", -width, 0, 0, -height, width, 0, "z"];
                }
                break;
            case "soft":
                var r;
                if (!dir) {
                    r = Math.min(width, Math.round(height / 5));
                    path = ["M", x + .5, y + .5 - Math.floor(height / 2), "l", width - r, 0, "a", r, r, 0, 0, 1, r, r, "l", 0, height - r * 2, "a", r, r, 0, 0, 1, -r, r, "l", r - width, 0, "z"];
                } else {
                    r = Math.min(Math.round(width / 5), height);
                    path = ["M", x - Math.floor(width / 2), y, "l", 0, r - height, "a", r, r, 0, 0, 1, r, -r, "l", width - 2 * r, 0, "a", r, r, 0, 0, 1, r, r, "l", 0, height - r, "z"];
                }
        }
        if (isPath) {
            return path.join(",");
        } else {
            return this.path({}, path);
        }
    };

    // Symbols
    Raphael.fn.g.disc = function (cx, cy, r) {
        return this.circle(cx, cy, r);
    };
    Raphael.fn.g.line = function (cx, cy, r) {
        return this.rect(cx - r, cy - r / 5, 2 * r, 2 * r / 5);
    };
    Raphael.fn.g.square = function (cx, cy, r) {
        r = r * .7;
        return this.rect(cx - r, cy - r, 2 * r, 2 * r);
    };
    Raphael.fn.g.triangle = function (cx, cy, r) {
        r *= 1.75;
        return this.path({}, "M".concat(cx, ",", cy, "m0-", r * .58, "l", r * .5, ",", r * .87, "-", r, ",0z"));
    };
    Raphael.fn.g.star = function (cx, cy, r, r2) {
        r2 = r2 || r * .5;
        var points = ["M", cx, cy + r2, "L"],
                R;
        for (var i = 1; i < 10; i++) {
            R = i % 2 ? r : r2;
            points = points.concat([(cx + R * Math.sin(i * Math.PI * .2)).toFixed(3), (cy + R * Math.cos(i * Math.PI * .2)).toFixed(3)]);
        }
        points.push("z");
        return this.path({}, points);
    };
    Raphael.fn.g.cross = function (cx, cy, r) {
        r = r / 2.5;
        return this.path({}, "M".concat(cx - r, ",", cy, "l", [-r, -r, r, -r, r, r, r, -r, r, r, -r, r, r, r, -r, r, -r, -r, -r, r, -r, -r, "z"]));
    };
    Raphael.fn.g.plus = function (cx, cy, r) {
        r = r / 2;
        return this.path({}, "M".concat(cx - r / 2, ",", cy - r / 2, "l", [0, -r, r, 0, 0, r, r, 0, 0, r, -r, 0, 0, r, -r, 0, 0, -r, -r, 0, 0, -r, "z"]));
    };
    Raphael.fn.g.arrow = function (cx, cy, r) {
        return this.path({}, "M".concat(cx - r * .7, ",", cy - r * .4, "l", [r * .6, 0, 0, -r * .4, r, r * .8, -r, r * .8, 0, -r * .4, -r * .6, 0], "z"));
    };

    // Tooltips
    Raphael.fn.g.tag = function (x, y, text, angle, r) {
        angle = angle || 0;
        r = r == null ? 5 : r;
        text = text == null ? "$9.99" : text;
        var R = .5522 * r,
                res = this.set(),
                d = 3;
        res.push(this.path({fill: "#000", stroke: "none"}));
        res.push(this.text(x, y, text).attr(this.g.txtattr).attr({fill: "#fff"}));
        res.update = function () {
            this.rotate(0, x, y);
            var bb = this[1].getBBox();
            if (bb.height >= r * 2) {
                this[0].attr({path: ["M", x, y + r, "a", r, r, 0, 1, 1, 0, -r * 2, r, r, 0, 1, 1, 0, r * 2, "m", 0, -r * 2 - d, "a", r + d, r + d, 0, 1, 0, 0, (r + d) * 2, "L", x + r + d, y + bb.height / 2 + d, "l", bb.width + 2 * d, 0, 0, -bb.height - 2 * d, -bb.width - 2 * d, 0, "L", x, y - r - d].join(",")});
            } else {
                var dx = Math.sqrt(Math.pow(r + d, 2) - Math.pow(bb.height / 2 + d, 2));
                // ["c", -R, 0, -r, R - r, -r, -r, 0, -R, r - R, -r, r, -r, R, 0, r, r - R, r, r, 0, R, R - r, r, -r, r]
                // "a", r, r, 0, 1, 1, 0, -r * 2, r, r, 0, 1, 1, 0, r * 2,
                this[0].attr({path: ["M", x, y + r, "c", -R, 0, -r, R - r, -r, -r, 0, -R, r - R, -r, r, -r, R, 0, r, r - R, r, r, 0, R, R - r, r, -r, r, "M", x + dx, y - bb.height / 2 - d, "a", r + d, r + d, 0, 1, 0, 0, bb.height + 2 * d, "l", r + d - dx + bb.width + 2 * d, 0, 0, -bb.height - 2 * d, "L", x + dx, y - bb.height / 2 - d].join(",")});
            }
            this[1].attr({x: x + r + d + bb.width / 2, y: y});
            angle = (360 - angle) % 360;
            this.rotate(angle, x, y);
            angle > 90 && angle < 270 && this[1].attr({x: x - r - d - bb.width / 2, y: y, rotation: [180 + angle, x, y]});
            return this;
        };
        res.update();
        return res;
    };
    Raphael.fn.g.popupit = function (x, y, set, dir, size) {
        dir = dir == null ? 2 : dir;
        size = size || 5;
        x = Math.round(x) + .5;
        y = Math.round(y) + .5;
        var bb = set.getBBox(),
                w = Math.round(bb.width / 2),
                h = Math.round(bb.height / 2),
                dx = [0, w + size * 2, 0, -w - size * 2],
                dy = [-h * 2 - size * 3, -h - size, 0, -h - size],
                p = ["M", x - dx[dir], y - dy[dir], "l", -size, (dir == 2) * -size, -Math.max(w - size, 0), 0, "a", size, size, 0, 0, 1, -size, -size,
                    "l", 0, -Math.max(h - size, 0), (dir == 3) * -size, -size, (dir == 3) * size, -size, 0, -Math.max(h - size, 0), "a", size, size, 0, 0, 1, size, -size,
                    "l", Math.max(w - size, 0), 0, size, !dir * -size, size, !dir * size, Math.max(w - size, 0), 0, "a", size, size, 0, 0, 1, size, size,
                    "l", 0, Math.max(h - size, 0), (dir == 1) * size, size, (dir == 1) * -size, size, 0, Math.max(h - size, 0), "a", size, size, 0, 0, 1, -size, size,
                    "l", -Math.max(w - size, 0), 0, "z"].join(","),
                xy = [
                    {
                        x: x,
                        y: y + size * 2 + h
                    },
                    {
                        x: x - size * 2 - w,
                        y: y
                    },
                    {
                        x: x,
                        y: y - size * 2 - h
                    },
                    {
                        x: x + size * 2 + w,
                        y: y
                    }
                ][dir];
        set.translate(xy.x - w - bb.x, xy.y - h - bb.y);
        return this.path({fill: "#000", stroke: "none"}, p).insertBefore(set.node ? set : set[0]);
    };
    Raphael.fn.g.popup = function (x, y, text, dir, size) {
        dir = dir == null ? 2 : dir;
        size = size || 5;
        text = text || "$9.99";
        var res = this.set(),
                d = 3;
        res.push(this.path({fill: "#000", stroke: "none"}));
        res.push(this.text(x, y, text).attr(this.g.txtattr).attr({fill: "#fff"}));
        res.update = function (X, Y, withAnimation) {
            X = X || x;
            Y = Y || y;
            var bb = this[1].getBBox(),
                    w = bb.width / 2,
                    h = bb.height / 2,
                    dx = [0, w + size * 2, 0, -w - size * 2],
                    dy = [-h * 2 - size * 3, -h - size, 0, -h - size],
                    p = ["M", X - dx[dir], Y - dy[dir], "l", -size, (dir == 2) * -size, -Math.max(w - size, 0), 0, "a", size, size, 0, 0, 1, -size, -size,
                        "l", 0, -Math.max(h - size, 0), (dir == 3) * -size, -size, (dir == 3) * size, -size, 0, -Math.max(h - size, 0), "a", size, size, 0, 0, 1, size, -size,
                        "l", Math.max(w - size, 0), 0, size, !dir * -size, size, !dir * size, Math.max(w - size, 0), 0, "a", size, size, 0, 0, 1, size, size,
                        "l", 0, Math.max(h - size, 0), (dir == 1) * size, size, (dir == 1) * -size, size, 0, Math.max(h - size, 0), "a", size, size, 0, 0, 1, -size, size,
                        "l", -Math.max(w - size, 0), 0, "z"].join(","),
                    xy = [
                        {
                            x: X,
                            y: Y + size * 2 + h
                        },
                        {
                            x: X - size * 2 - w,
                            y: Y
                        },
                        {
                            x: X,
                            y: Y - size * 2 - h
                        },
                        {
                            x: X + size * 2 + w,
                            y: Y
                        }
                    ][dir];
            if (withAnimation) {
                this[0].animate({path: p}, 500, ">");
                this[1].animate(xy, 500, ">");
            } else {
                this[0].attr({path: p});
                this[1].attr(xy);
            }
            return this;
        };
        return res.update(x, y);
    };
    Raphael.fn.g.flag = function (x, y, text, angle) {
        angle = angle || 0;
        text = text || "$9.99";
        var res = this.set(),
                d = 3;
        res.push(this.path({fill: "#000", stroke: "none"}));
        res.push(this.text(x, y, text).attr(this.g.txtattr).attr({fill: "#fff"}));
        res.update = function (x, y) {
            this.rotate(0, x, y);
            var bb = this[1].getBBox(),
                    h = bb.height / 2;
            this[0].attr({path: ["M", x, y, "l", h + d, -h - d, bb.width + 2 * d, 0, 0, bb.height + 2 * d, -bb.width - 2 * d, 0, "z"].join(",")});
            this[1].attr({x: x + h + d + bb.width / 2, y: y});
            angle = 360 - angle;
            this.rotate(angle, x, y);
            angle > 90 && angle < 270 && this[1].attr({x: x - r - d - bb.width / 2, y: y, rotation: [180 + angle, x, y]});
            return this;
        };
        return res.update(x, y);
    };
    Raphael.fn.g.label = function (x, y, text) {
        var res = this.set();
        res.push(this.rect(x, y, 10, 10).attr({stroke: "none", fill: "#000"}));
        res.push(this.text(x, y, text).attr(this.g.txtattr).attr({fill: "#fff"}));
        res.update = function () {
            var bb = this[1].getBBox(),
                    r = Math.min(bb.width + 10, bb.height + 10) / 2;
            this[0].attr({x: bb.x - r / 2, y: bb.y - r / 2, width: bb.width + r, height: bb.height + r, r: r});
        };
        res.update();
        return res;
    };
    Raphael.fn.g.labelit = function (set) {
        var bb = set.getBBox(),
                r = Math.min(20, bb.width + 10, bb.height + 10) / 2;
        return this.rect(bb.x - r / 2, bb.y - r / 2, bb.width + r, bb.height + r, r).attr({stroke: "none", fill: "#000"}).insertBefore(set[0]);
    };
    Raphael.fn.g.drop = function (x, y, text, size, angle) {
        size = size || 30;
        angle = angle || 0;
        var res = this.set();
        res.push(this.path({}, ["M", x, y, "l", size, 0, "A", size * .4, size * .4, 0, 1, 0, x + size * .7, y - size * .7, "z"]).attr({fill: "#000", stroke: "none", rotation: [22.5 - angle, x, y]}));
        angle = (angle + 90) * Math.PI / 180;
        res.push(this.text(x + size * Math.sin(angle), y + size * Math.cos(angle), text).attr(this.g.txtattr).attr({"font-size": size * 12 / 30, fill: "#fff"}));
        res.drop = res[0];
        res.text = res[1];
        return res;
    };
    Raphael.fn.g.blob = function (x, y, text, angle) {
        var angle = (+angle + 1 ? angle : 45) + 90,
                size = 12,
                rad = Math.PI / 180,
                fontSize = size * 12 / 12;
        var res = this.set();
        res.push(this.path({fill: "#000", stroke: "none"}));
        res.push(this.text(x + size * Math.sin((angle) * rad), y + size * Math.cos((angle) * rad) - fontSize / 2, text).attr(this.g.txtattr).attr({"font-size": fontSize, fill: "#fff"}));
        res.update = function (X, Y, withAnimation) {
            X = X || x;
            Y = Y || y;
            var bb = this[1].getBBox(),
                    w = Math.max(bb.width + fontSize, size * 25 / 12),
                    h = Math.max(bb.height + fontSize, size * 25 / 12),
                    x2 = X + size * Math.sin((angle - 22.5) * rad),
                    y2 = Y + size * Math.cos((angle - 22.5) * rad),
                    x1 = X + size * Math.sin((angle + 22.5) * rad),
                    y1 = Y + size * Math.cos((angle + 22.5) * rad),
                    dx = (x1 - x2) / 2,
                    dy = (y1 - y2) / 2,
                    rx = w / 2,
                    ry = h / 2,
                    k = -Math.sqrt(Math.abs(rx * rx * ry * ry - rx * rx * dy * dy - ry * ry * dx * dx) / (rx * rx * dy * dy + ry * ry * dx * dx)),
                    cx = k * rx * dy / ry + (x1 + x2) / 2,
                    cy = k * -ry * dx / rx + (y1 + y2) / 2;
            if (withAnimation) {
                this.animate({x: cx, y: cy, path: ["M", x, y, "L", x1, y1, "A", rx, ry, 0, 1, 1, x2, y2, "z"].join(",")}, 500, ">");
            } else {
                this.attr({x: cx, y: cy, path: ["M", x, y, "L", x1, y1, "A", rx, ry, 0, 1, 1, x2, y2, "z"].join(",")});
            }
            return this;
        };
        res.update(x, y);
        return res;
    };

    Raphael.fn.g.colorValue = function (value, total, s, b) {
        return "hsb(" + [Math.min((1 - value / total) * .4, 1), s || .75, b || .75] + ")";
    };

    Raphael.fn.g.snapEnds = function (from, to, steps) {
        var f = from,
                t = to;
        if (f == t) {
            return {from: f, to: t, power: 0};
        }
        function round(a) {
            return Math.abs(a - .5) < .25 ? Math.floor(a) + .5 : Math.round(a);
        }

        var d = (t - f) / steps,
                r = Math.floor(d),
                R = r,
                i = 0;
        if (r) {
            while (R) {
                i--;
                R = Math.floor(d * Math.pow(10, i)) / Math.pow(10, i);
            }
            i ++;
        } else {
            while (!r) {
                i = i || 1;
                r = Math.floor(d * Math.pow(10, i)) / Math.pow(10, i);
                i++;
            }
            i && i--;
        }
        var t = round(to * Math.pow(10, i)) / Math.pow(10, i);
        if (t < to) {
            t = round((to + .5) * Math.pow(10, i)) / Math.pow(10, i);
        }
        var f = round((from - (i > 0 ? 0 : .5)) * Math.pow(10, i)) / Math.pow(10, i);
        return {from: f, to: t, power: i};
    };
    Raphael.fn.g.axis = function (x, y, length, from, to, steps, orientation, labels, type, dashsize) {
        dashsize = dashsize == null ? 2 : dashsize;
        type = type || "t";
        steps = steps || 10;
        var path = type == "|" || type == " " ? ["M", x + .5, y, "l", 0, .001] : orientation == 1 || orientation == 3 ? ["M", x + .5, y, "l", 0, -length] : ["M", x, y + .5, "l", length, 0],
                ends = this.g.snapEnds(from, to, steps),
                f = ends.from,
                t = ends.to,
                i = ends.power,
                j = 0,
                text = this.set();
        d = (t - f) / steps;
        var label = f,
                rnd = i > 0 ? i : 0;
        dx = length / steps;
        if (+orientation == 1 || +orientation == 3) {
            var Y = y,
                    addon = (orientation - 1 ? 1 : -1) * (dashsize + 3 + !!(orientation - 1));
            while (Y >= y - length) {
                type != "-" && type != " " && (path = path.concat(["M", x - (type == "+" || type == "|" ? dashsize : !(orientation - 1) * dashsize * 2), Y + .5, "l", dashsize * 2 + 1, 0]));
                text.push(this.text(x + addon, Y, (labels && labels[j++]) || (Math.round(label) == label ? label : +label.toFixed(rnd))).attr(this.g.txtattr).attr({"text-anchor": orientation - 1 ? "start" : "end"}));
                label += d;
                Y -= dx;
            }
            if (Math.round(Y + dx - (y - length))) {
                type != "-" && type != " " && (path = path.concat(["M", x - (type == "+" || type == "|" ? dashsize : !(orientation - 1) * dashsize * 2), y - length + .5, "l", dashsize * 2 + 1, 0]));
                text.push(this.text(x + addon, y - length, (labels && labels[j]) || (Math.round(label) == label ? label : +label.toFixed(rnd))).attr(this.g.txtattr).attr({"text-anchor": orientation - 1 ? "start" : "end"}));
            }
        } else {
            var X = x,
                    label = f,
                    rnd = i > 0 ? i : 0,
                    addon = (orientation ? -1 : 1) * (dashsize + 9 + !orientation),
                    dx = length / steps,
                    txt = 0,
                    prev = 0;
            while (X <= x + length) {
                type != "-" && type != " " && (path = path.concat(["M", X + .5, y - (type == "+" ? dashsize : !!orientation * dashsize * 2), "l", 0, dashsize * 2 + 1]));
                text.push(txt = this.text(X, y + addon, (labels && labels[j++]) || (Math.round(label) == label ? label : +label.toFixed(rnd))).attr(this.g.txtattr));
                var bb = txt.getBBox();
                if (prev >= bb.x - 5) {
                    text.pop(text.length - 1).remove();
                } else {
                    prev = bb.x + bb.width;
                }
                label += d;
                X += dx;
            }
            if (Math.round(X - dx - x - length)) {
                type != "-" && type != " " && (path = path.concat(["M", x + length + .5, y - (type == "+" ? dashsize : !!orientation * dashsize * 2), "l", 0, dashsize * 2 + 1]));
                text.push(this.text(x + length, y + addon, (labels && labels[j]) || (Math.round(label) == label ? label : +label.toFixed(rnd))).attr(this.g.txtattr));
            }
        }
        var res = this.path({}, path);
        res.text = text;
        res.all = this.set([res, text]);
        res.remove = function () {
            this.text.remove();
            this.constructor.prototype.remove.call(this);
        };
        return res;
    };

    Raphael.el.lighter = function (times) {
        times = times || 2;
        var fs = [this.attrs.fill, this.attrs.stroke];
        this.fs = this.fs || [fs[0], fs[1]];
        fs[0] = Raphael.rgb2hsb(Raphael.getRGB(fs[0]).hex);
        fs[1] = Raphael.rgb2hsb(Raphael.getRGB(fs[1]).hex);
        fs[0].b = Math.min(fs[0].b * times, 1);
        fs[0].s = fs[0].s / times;
        fs[1].b = Math.min(fs[1].b * times, 1);
        fs[1].s = fs[1].s / times;
        this.attr({fill: "hsb(" + [fs[0].h, fs[0].s, fs[0].b] + ")", stroke: "hsb(" + [fs[1].h, fs[1].s, fs[1].b] + ")"});
    };
    Raphael.el.darker = function (times) {
        times = times || 2;
        var fs = [this.attrs.fill, this.attrs.stroke];
        this.fs = this.fs || [fs[0], fs[1]];
        fs[0] = Raphael.rgb2hsb(Raphael.getRGB(fs[0]).hex);
        fs[1] = Raphael.rgb2hsb(Raphael.getRGB(fs[1]).hex);
        fs[0].s = Math.min(fs[0].s * times, 1);
        fs[0].b = fs[0].b / times;
        fs[1].s = Math.min(fs[1].s * times, 1);
        fs[1].b = fs[1].b / times;
        this.attr({fill: "hsb(" + [fs[0].h, fs[0].s, fs[0].b] + ")", stroke: "hsb(" + [fs[1].h, fs[1].s, fs[1].b] + ")"});
    };
    Raphael.el.original = function () {
        if (this.fs) {
            this.attr({fill: this.fs[0], stroke: this.fs[1]});
            delete this.fs;
        }
    };
})();
;
/* END /2static/script/lib/graphael/g.raphael.js */
/* START /2static/script/lib/graphael/g.bar.js */
/*
 * g.Raphael 0.2 - Charting library, based on Raphaël
 *
 * Copyright (c) 2009 Dmitry Baranovskiy (http://g.raphaeljs.com)
 * Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license.
 */

Raphael.fn.g.barchart = function (x, y, width, height, values, isVertical, opts) {
    opts = opts || {};
    var type = {round: "round", sharp: "sharp", soft: "soft"}[opts.type] || "square",
            gutter = parseFloat(opts.gutter || "20%"),
            chart = this.set(),
            bars = this.set(),
            covers = this.set(),
            covers2 = this.set(),
            total = Math.max.apply(Math, values),
            stacktotal = [],
            paper = this,
            multi = 0,
            colors = opts.colors || this.g.colors,
            len = values.length;
    if (this.raphael.isArray(values[0])) {
        total = [];
        multi = len;
        len = 0;
        for (var i = values.length; i--;) {
            total.push(Math.max.apply(Math, values[i]));
            len = Math.max(len, values[i].length);
        }
        if (opts.stacked) {
            for (var i = len; i--;) {
                var tot = 0;
                for (var j = values.length; j--;) {
                    tot += + values[j][i] || 0;
                }
                stacktotal.push(tot);
            }
        }
        for (var i = values.length; i--;) {
            if (values[i].length < len) {
                for (var j = len; j--;) {
                    values[i].push(0);
                }
            }
        }
        total = Math.max.apply(Math, opts.stacked ? stacktotal : total);
    }

    total = (opts.to) || total;
    if (!isVertical) {
        var barwidth = width / (len * (100 + gutter) + gutter) * 100,
                barhgutter = barwidth * gutter / 100,
                barvgutter = typeof opts.vgutter == "undefined" ? 20 : opts.vgutter,
                stack = [],
                X = x + barhgutter,
                Y = (height - 2 * barvgutter) / total;
        if (!opts.stretch) {
            barhgutter = Math.round(barhgutter);
            barwidth = Math.floor(barwidth);
        }
        !opts.stacked && (barwidth /= multi || 1);
        for (var i = 0; i < len; i++) {
            stack = [];
            for (var j = 0; j < multi; j++) {
                var h = Math.round((multi ? values[j][i] : values[i]) * Y),
                        top = y + height - barvgutter - h,
                        bar;
                bars.push(bar = this.g.finger(Math.round(X + barwidth / 2), top + h, barwidth, opts.init ? 0 : h, true, type).attr({stroke: "none", fill: colors[multi ? j : i]}));
                bar.y = top;
                bar.x = Math.round(X + barwidth / 2);
                bar.w = barwidth;
                bar.h = h;
                bar.value = multi ? values[j][i] : values[i];
                opts.init && bar.animate({path: this.g.finger(Math.round(X + barwidth / 2), top + h, barwidth, h, true, type, 1)}, (+opts.init - 1) || 1000, ">");
                if (!opts.stacked) {
                    X += barwidth;
                } else {
                    stack.push(bar);
                }
            }
            if (opts.stacked) {
                var cvr;
                covers2.push(cvr = this.rect(stack[0].x - stack[0].w / 2, y, barwidth, height).attr({stroke: "none", fill: "#000", opacity: 0}));
                cvr.bars = [];
                for (var s = 0, ss = stack.length; s < ss; s++) {
                    cvr.bars.push(stack[s]);
                }
                stack.sort(function (a, b) {
                    return a.value - b.value;
                });
                var size = 0;
                for (var s = stack.length; s--;) {
                    stack[s].toFront();
                }
                for (var s = 0, ss = stack.length; s < ss; s++) {
                    var bar = stack[s],
                            cover,
                            h = (size + bar.value) * Y,
                            path = this.g.finger(bar.x, y + height - barvgutter - !!size * .5, barwidth, h, true, type, 1);
                    size && opts.init && bar.animate({path: path}, (+opts.init - 1) || 1000, ">");
                    size && !opts.init && bar.attr({path: path});
                    bar.h = h;
                    bar.y = y + height - barvgutter - !!size * .5 - h;
                    covers.push(cover = this.rect(bar.x - bar.w / 2, bar.y, barwidth, bar.value * Y).attr({stroke: "none", fill: "#000", opacity: 0}));
                    cover.bar = bar;
                    size += bar.value;
                }

                X += barwidth;
            }
            X += barhgutter;
        }
        covers2.toFront();
        X = x + barhgutter;
        if (!opts.stacked) {
            for (var i = 0; i < len; i++) {
                for (var j = 0; j < multi; j++) {
                    var cover;
                    covers.push(cover = this.rect(Math.round(X), y + barvgutter, barwidth, height - barvgutter).attr({stroke: "none", fill: "#000", opacity: 0}));
                    cover.bar = bars[i * (multi || 1) + j];
                    X += barwidth;
                }
                X += barhgutter;
            }
        }
        chart.label = function (labels, isBottom) {
            labels = labels || [];
            this.labels = paper.set();
            var L, l = -Infinity;
            if (opts.stacked) {
                for (var i = 0; i < len; i++) {
                    var tot = 0;
                    for (var j = 0; j < multi; j++) {
                        tot += multi ? values[j][i] : values[i];
                        if (j == multi - 1) {
                            var label = paper.g.labelise(labels[i], tot, total);
                            L = paper.g.text(bars[i * (multi || 1) + j].x, y + height - barvgutter / 2, label).insertBefore(covers[i * (multi || 1) + j]);
                            var bb = L.getBBox();
                            if (bb.x - 7 < l) {
                                L.remove();
                            } else {
                                this.labels.push(L);
                                l = bb.x + bb.width;
                            }
                        }
                    }
                }
            } else {
                for (var i = 0; i < len; i++) {
                    for (var j = 0; j < multi; j++) {
                        var label = paper.g.labelise(multi ? labels[j] && labels[j][i] : labels[i], multi ? values[j][i] : values[i], total);
                        L = paper.g.text(bars[i * (multi || 1) + j].x, isBottom ? y + height - barvgutter / 2 : bars[i * (multi || 1) + j].y - 10, label).insertBefore(covers[i * (multi || 1) + j]);
                        var bb = L.getBBox();
                        if (bb.x - 7 < l) {
                            L.remove();
                        } else {
                            this.labels.push(L);
                            l = bb.x + bb.width;
                        }
                    }
                }
            }
            return this;
        };
    } else {
        var barheight = Math.floor(height / (len * (100 + gutter) + gutter) * 100),
                bargutter = Math.floor(barheight * gutter / 100),
                stack = [],
                Y = y + bargutter,
                X = (width - 1) / total;
        !opts.stacked && (barheight /= multi || 1);
        for (var i = 0; i < len; i++) {
            stack = [];
            for (var j = 0; j < multi; j++) {
                var val = multi ? values[j][i] : values[i],
                        bar;
                bars.push(bar = this.g.finger(x, Y + barheight / 2, opts.init ? 0 : Math.round(val * X), barheight - 1, false, type).attr({stroke: colors[multi > 1 ? j : i], fill: colors[multi > 1 ? j : i]}));
                bar.x = x + Math.round(val * X);
                bar.y = Y + barheight / 2;
                bar.w = Math.round(val * X);
                bar.h = barheight;
                bar.value = +val;
                opts.init && bar.animate({path: this.g.finger(x, Y + barheight / 2, Math.round(val * X), barheight - 1, false, type, 1)}, (+opts.init - 1) || 500, ">");
                if (!opts.stacked) {
                    Y += barheight;
                } else {
                    stack.push(bar);
                }
            }
            if (opts.stacked) {
                stack.sort(function (a, b) {
                    return a.value - b.value;
                });
                var size = 0;
                for (var s = stack.length; s--;) {
                    stack[s].toFront();
                }
                for (var s = 0, ss = stack.length; s < ss; s++) {
                    var bar = stack[s],
                            cover,
                            val = Math.round((size + bar.value) * X),
                            path = this.g.finger(x, bar.y, val, barheight - 1, false, type, 1);
                    size && opts.init && bar.animate({path: path}, (+opts.init - 1) || 1000, ">");
                    size && !opts.init && bar.attr({path: path});
                    bar.w = val;
                    bar.x = x + val;
                    covers.push(cover = this.rect(x + size * X, bar.y - bar.h / 2, bar.value * X, barheight).attr({stroke: "none", fill: "#000", opacity: 0}));
                    cover.bar = bar;
                    size += bar.value;
                }
                Y += barheight;
            }
            Y += bargutter;
        }
        Y = y + bargutter;
        if (!opts.stacked) {
            for (var i = 0; i < len; i++) {
                for (var j = 0; j < multi; j++) {
                    covers.push(this.rect(x, Y, width, barheight).attr({stroke: "none", fill: "#000", opacity: 0}));
                    Y += barheight;
                }
                Y += bargutter;
            }
        }
        chart.label = function (labels, isRight) {
            labels = labels || [];
            this.labels = paper.set();
            for (var i = 0; i < len; i++) {
                for (var j = 0; j < multi; j++) {
                    var label = paper.g.labelise(multi ? labels[j] && labels[j][i] : labels[i], multi ? values[j][i] : values[i], total);
                    var X = isRight ? bars[i * (multi || 1) + j].x - barheight / 2 + 3 : x + 5,
                            A = isRight ? "end" : "start",
                            L;
                    this.labels.push(L = paper.g.text(X, bars[i * (multi || 1) + j].y, label).attr({"text-anchor": A}).insertBefore(covers[0]));
                    if (L.getBBox().x < x + 5) {
                        L.attr({x: x + 5, "text-anchor": "start"});
                    } else {
                        bars[i * (multi || 1) + j].label = L;
                    }
                }
            }
            return this;
        };
    }
    chart.hover = function (fin, fout) {
        covers2.hide();
        covers.show();
        fout = fout || function () {
        };
        covers.mouseover(fin).mouseout(fout);
        return this;
    };
    chart.hoverColumn = function (fin, fout) {
        covers.hide();
        covers2.show();
        fout = fout || function () {
        };
        covers2.mouseover(fin).mouseout(fout);
        return this;
    };
    chart.click = function (f) {
        covers2.hide();
        covers.show();
        covers.click(f);
        return this;
    };
    chart.clickColumn = function (f) {
        covers.hide();
        covers2.show();
        covers2.click(f);
        return this;
    };
    chart.push(bars, covers, covers2);
    chart.bars = bars;
    chart.covers = covers;
    return chart;
};
;
/* END /2static/script/lib/graphael/g.bar.js */
/* START /2static/script/lib/graphael/g.pie.js */
/*
 * g.Raphael 0.2 - Charting library, based on Raphaël
 *
 * Copyright (c) 2009 Dmitry Baranovskiy (http://g.raphaeljs.com)
 * Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license.
 */

Raphael.fn.g.piechart = function (cx, cy, r, values, opts) {
    opts = opts || {};
    var paper = this,
            sectors = [],
            covers = this.set(),
            chart = this.set(),
            series = this.set(),
            order = [],
            len = values.length,
            angle = 0,
            total = 0,
            others = 0,
            cut = 9,
            defcut = true;
    chart.covers = covers;
    if (len == 1) {
        series.push(this.circle(cx, cy, r).attr({fill: this.g.colors[0], stroke: opt.stroke || "#fff", "stroke-width": opts.strokewidth == null ? 1 : opts.strokewidth}));
        covers.push(this.circle(cx, cy, r).attr({fill: "#000", opacity: 0, "stroke-width": 3}));
        total = values[0];
        values[0] = {value: values[0], order: 0, valueOf: function () {
            return this.value;
        }};
        series[0].middle = {x: cx, y: cy};
        series[0].mangle = 180;
    } else {
        function sector(cx, cy, r, startAngle, endAngle, fill) {
            var rad = Math.PI / 180,
                    x1 = cx + r * Math.cos(-startAngle * rad),
                    x2 = cx + r * Math.cos(-endAngle * rad),
                    xm = cx + r / 2 * Math.cos(-(startAngle + (endAngle - startAngle) / 2) * rad),
                    y1 = cy + r * Math.sin(-startAngle * rad),
                    y2 = cy + r * Math.sin(-endAngle * rad),
                    ym = cy + r / 2 * Math.sin(-(startAngle + (endAngle - startAngle) / 2) * rad),
                    res = ["M", cx, cy, "L", x1, y1, "A", r, r, 0, +(Math.abs(endAngle - startAngle) > 180), 1, x2, y2, "z"];
            res.middle = {x: xm, y: ym};
            return res;
        }

        for (var i = 0; i < len; i++) {
            total += values[i];
            values[i] = {value: values[i], order: i, valueOf: function () {
                return this.value;
            }};
        }
        values.sort(function (a, b) {
            return b.value - a.value;
        });
        for (var i = 0; i < len; i++) {
            if (defcut && values[i] * 360 / total <= 1.5) {
                cut = i;
                defcut = false;
            }
            if (i > cut) {
                defcut = false;
                values[cut].value += values[i];
                values[cut].others = true;
                others = values[cut].value;
            }
        }
        len = Math.min(cut + 1, values.length);
        others && values.splice(len) && (values[cut].others = true);
        for (var i = 0; i < len; i++) {
            var mangle = angle - 360 * values[i] / total / 2;
            if (!i) {
                angle = 90 - mangle;
                mangle = angle - 360 * values[i] / total / 2;
            }
            if (opts.init) {
                var ipath = sector(cx, cy, 1, angle, angle - 360 * values[i] / total).join(",");
            }
            var path = sector(cx, cy, r, angle, angle -= 360 * values[i] / total);
            var p = this.path({fill: opts.colors && opts.colors[i] || this.g.colors[i] || "#666", stroke: opts.stroke || "#fff", "stroke-width": opts.strokewidth == null ? 1 : opts.strokewidth, "stroke-linejoin": "round"}, opts.init ? ipath : path.join(","));
            p.value = values[i];
            p.middle = path.middle;
            p.mangle = mangle;
            sectors.push(p);
            series.push(p);
            opts.init && p.animate({path: path.join(",")}, (+opts.init - 1) || 1000, ">");
        }
        for (var i = 0; i < len; i++) {
            var p = paper.path({fill: "#000", opacity: 0, "stroke-width": 3}, sectors[i].attr("path"));
            opts.href && opts.href[i] && p.attr({href: opts.href[i]});
            p.attr = function () {
            };
            covers.push(p);
            series.push(p);
        }
    }

    chart.hover = function (fin, fout) {
        fout = fout || function () {
        };
        var that = this;
        for (var i = 0; i < len; i++) {
            (function (sector, cover, j) {
                var o = {
                    sector: sector,
                    cover: cover,
                    cx: cx,
                    cy: cy,
                    mx: sector.middle.x,
                    my: sector.middle.y,
                    mangle: sector.mangle,
                    r: r,
                    value: values[j],
                    total: total,
                    label: that.labels && that.labels[j]
                };
                cover.mouseover(function () {
                    fin.call(o);
                }).mouseout(function () {
                    fout.call(o);
                });
            })(series[i], covers[i], i);
        }
        return this;
    };
    chart.click = function (f) {
        var that = this;
        for (var i = 0; i < len; i++) {
            (function (sector, cover, j) {
                var o = {
                    sector: sector,
                    cover: cover,
                    cx: cx,
                    cy: cy,
                    mx: sector.middle.x,
                    my: sector.middle.y,
                    mangle: sector.mangle,
                    r: r,
                    value: values[j],
                    total: total,
                    label: that.labels && that.labels[j]
                };
                cover.click(function () {
                    f.call(o);
                });
            })(series[i], covers[i], i);
        }
        return this;
    };
    chart.inject = function (element) {
        element.insertBefore(covers[0]);
    };
    var legend = function (labels, otherslabel, mark, dir) {
        var x = cx + r + r / 5,
                y = cy,
                h = y + 10;
        labels = labels || [];
        dir = (dir && dir.toLowerCase && dir.toLowerCase()) || "east";
        mark = paper.g.markers[mark && mark.toLowerCase()] || "disc";
        chart.labels = paper.set();
        for (var i = 0; i < len; i++) {
            var clr = series[i].attr("fill"),
                    j = values[i].order,
                    txt;
            values[i].others && (labels[j] = otherslabel || "Others");
            labels[j] = paper.g.labelise(labels[j], values[i], total);
            chart.labels.push(paper.set());
            chart.labels[i].push(paper.g[mark](x + 5, h, 5).attr({fill: clr, stroke: "none"}));
            chart.labels[i].push(txt = paper.text(x + 20, h, labels[j] || values[j]).attr(paper.g.txtattr).attr({fill: opts.legendcolor || "#000", "text-anchor": "start"}));
            covers[i].label = chart.labels[i];
            h += txt.getBBox().height * 1.2;
        }
        var bb = chart.labels.getBBox(),
                tr = {
                    east: [0, -bb.height / 2],
                    west: [-bb.width - 2 * r - 20, -bb.height / 2],
                    north: [-r - bb.width / 2, -r - bb.height - 10],
                    south: [-r - bb.width / 2, r + 10]
                }[dir];
        chart.labels.translate.apply(chart.labels, tr);
        chart.push(chart.labels);
    };
    if (opts.legend) {
        legend(opts.legend, opts.legendothers, opts.legendmark, opts.legendpos);
    }
    chart.push(series, covers);
    chart.series = series;
    chart.covers = covers;
    return chart;
};
;
/* END /2static/script/lib/graphael/g.pie.js */
/* START /2static/script/lib/graphael/g.line.js */
/*
 * g.Raphael 0.2 - Charting library, based on Raphaël
 *
 * Copyright (c) 2009 Dmitry Baranovskiy (http://g.raphaeljs.com)
 * Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license.
 */

Raphael.fn.g.linechart = function (x, y, width, height, valuesx, valuesy, opts) {
    function shrink(values, dim) {
        var k = values.length / dim,
                j = 0,
                l = k,
                sum = 0,
                res = [];
        while (j < values.length) {
            l--;
            if (l < 0) {
                sum += values[j] * (1 + l);
                res.push(sum / k);
                sum = values[j++] * -l;
                l += k;
            } else {
                sum += values[j++];
            }
        }
        return res;
    }

    opts = opts || {};
    if (!this.raphael.isArray(valuesx[0])) {
        valuesx = [valuesx];
    }
    if (!this.raphael.isArray(valuesy[0])) {
        valuesy = [valuesy];
    }
    var allx = Array.prototype.concat.apply([], valuesx),
            ally = Array.prototype.concat.apply([], valuesy),
            xdim = this.g.snapEnds(Math.min.apply(Math, allx), Math.max.apply(Math, allx), valuesx[0].length - 1),
            minx = xdim.from,
            maxx = xdim.to,
            gutter = opts.gutter || 10,
            kx = (width - gutter * 2) / (maxx - minx),
            ydim = this.g.snapEnds(Math.min.apply(Math, ally), Math.max.apply(Math, ally), valuesy[0].length - 1),
            miny = ydim.from,
            maxy = ydim.to,
            ky = (height - gutter * 2) / (maxy - miny),
            len = Math.max(valuesx[0].length, valuesy[0].length),
            symbol = opts.symbol || "",
            colors = opts.colors || Raphael.fn.g.colors,
            that = this,
            columns = null,
            dots = null,
            chart = this.set(),
            path = [];

    for (var i = 0, ii = valuesy.length; i < ii; i++) {
        len = Math.max(len, valuesy[i].length);
    }
    var shades = this.set();
    for (var i = 0, ii = valuesy.length; i < ii; i++) {
        if (opts.shade) {
            shades.push(this.path({stroke: "none", fill: colors[i], opacity: opts.nostroke ? 1 : .3}));
        }
        if (valuesy[i].length > width - 2 * gutter) {
            valuesy[i] = shrink(valuesy[i], width - 2 * gutter);
            len = width - 2 * gutter;
        }
        if (valuesx[i] && valuesx[i].length > width - 2 * gutter) {
            valuesx[i] = shrink(valuesx[i], width - 2 * gutter);
        }
    }
    var axis = this.set();
    if (opts.axis) {
        var ax = (opts.axis + "").split(/[,\s]+/);
        +ax[0] && axis.push(this.g.axis(x + gutter, y + gutter, width - 2 * gutter, minx, maxx, opts.axisxstep || Math.floor((width - 2 * gutter) / 20), 2));
        +ax[1] && axis.push(this.g.axis(x + width - gutter, y + height - gutter, height - 2 * gutter, miny, maxy, opts.axisystep || Math.floor((height - 2 * gutter) / 20), 3));
        +ax[2] && axis.push(this.g.axis(x + gutter, y + height - gutter, width - 2 * gutter, minx, maxx, opts.axisxstep || Math.floor((width - 2 * gutter) / 20), 0));
        +ax[3] && axis.push(this.g.axis(x + gutter, y + height - gutter, height - 2 * gutter, miny, maxy, opts.axisystep || Math.floor((height - 2 * gutter) / 20), 1));
    }
    var lines = this.set(),
            symbols = this.set(),
            line;
    for (var i = 0, ii = valuesy.length; i < ii; i++) {
        if (!opts.nostroke) {
            lines.push(line = this.path({
                stroke: colors[i],
                "stroke-width": opts.width || 2,
                "stroke-linejoin": "round",
                "stroke-linecap": "round",
                "stroke-dasharray": opts.dash || ""
            }));
        }
        var sym = this.raphael.isArray(symbol) ? symbol[i] : symbol;
        path = [];
        for (var j = 0, jj = valuesy[i].length; j < jj; j++) {
            var X = x + gutter + ((valuesx[i] || valuesx[0])[j] - minx) * kx;
            var Y = y + height - gutter - (valuesy[i][j] - miny) * ky;
            (Raphael.isArray(sym) ? sym[j] : sym) && symbols.push(this.g[Raphael.fn.g.markers[this.raphael.isArray(sym) ? sym[j] : sym]](X, Y, (opts.width || 2) * 3).attr({fill: colors[i], stroke: "none"}));
            path = path.concat([j ? "L" : "M", X, Y]);
        }
        if (opts.shade) {
            shades[i].attr({path: path.concat(["L", X, y + height - gutter, "L",  x + gutter + ((valuesx[i] || valuesx[0])[0] - minx) * kx, y + height - gutter, "z"]).join(",")});
        }
        !opts.nostroke && line.attr({path: path.join(",")});
    }
    function createColumns() {
        // unite Xs together
        var Xs = [];
        for (var i = 0, ii = valuesx.length; i < ii; i++) {
            Xs = Xs.concat(valuesx[i]);
        }
        Xs.sort();
        // remove duplicates
        var Xs2 = [],
                xs = [];
        for (var i = 0, ii = Xs.length; i < ii; i++) {
            Xs[i] != Xs[i - 1] && Xs2.push(Xs[i]) && xs.push(x + gutter + (Xs[i] - minx) * kx);
        }
        Xs = Xs2;
        ii = Xs.length;
        var cvrs = that.set();
        for (var i = 0; i < ii; i++) {
            var X = xs[i] - (xs[i] - (xs[i - 1] || x)) / 2,
                    w = ((xs[i + 1] || x + width) - xs[i]) / 2 + (xs[i] - (xs[i - 1] || x)) / 2,
                    C;
            cvrs.push(C = that.rect(X - 1, y, w + 1, height).attr({stroke: "none", fill: "#000", opacity: 0}));
            C.values = [];
            C.y = [];
            C.x = xs[i];
            C.axis = Xs[i];
            C.xIndex = i;
            for (var j = 0, jj = valuesy.length; j < jj; j++) {
                Xs2 = valuesx[j] || valuesx[0];
                for (var k = 0, kk = Xs2.length; k < kk; k++) {
                    if (Xs2[k] == Xs[i]) {
                        C.values.push(valuesy[j][k]);
                        C.y.push(y + height - gutter - (valuesy[j][k] - miny) * ky);
                    }
                }
            }
        }
        columns = cvrs;
    }

    function createDots() {
        var cvrs = that.set(),
                C;
        for (var i = 0, ii = valuesy.length; i < ii; i++) {
            for (var j = 0, jj = valuesy[i].length; j < jj; j++) {
                var X = x + gutter + ((valuesx[i] || valuesx[0])[j] - minx) * kx,
                        nearX = x + gutter + ((valuesx[i] || valuesx[0])[j ? j - 1 : 1] - minx) * kx,
                        Y = y + height - gutter - (valuesy[i][j] - miny) * ky;
                cvrs.push(C = that.circle(X, Y, Math.abs(nearX - X) / 2).attr({stroke: "none", fill: "#000", opacity: 0}));
                C.x = X;
                C.y = Y;
                C.value = valuesy[i][j];
                C.axis = (valuesx[i] || valuesx[0])[j];
            }
        }
        dots = cvrs;
    }

    chart.push(axis, columns, dots, lines, shades, symbols);
    chart.hoverColumn = function (fin, fout) {
        !columns && createColumns();
        columns.mouseover(fin).mouseout(fout);
        return this;
    };
    chart.clickColumn = function (f) {
        !columns && createColumns();
        columns.click(f);
        return this;
    };
    chart.hrefColumn = function (cols) {
        var hrefs = that.raphael.isArray(arguments[0]) ? arguments[0] : arguments;
        if (!(arguments.length - 1) && typeof cols == "object") {
            for (var x in cols) {
                for (var i = 0, ii = columns.length; i < ii; i++) if (columns[i].axis == x) {
                    columns[i].attr("href", cols[x]);
                }
            }
        }
        !columns && createColumns();
        for (var i = 0, ii = hrefs.length; i < ii; i++) {
            columns[i] && columns[i].attr("href", hrefs[i]);
        }
        return this;
    };
    chart.hoverDot = function (fin, fout) {
        !dots && createDots();
        dots.mouseover(fin).mouseout(fout);
        return this;
    };
    chart.clickDot = function (f) {
        !dots && createDots();
        dots.click(f);
        return this;
    };
    return chart;
};
;
/* END /2static/script/lib/graphael/g.line.js */
/* START /2static/script/lib/graphael/g.dot.js */
/*
 * g.Raphael 0.2 - Charting library, based on Raphaël
 *
 * Copyright (c) 2009 Dmitry Baranovskiy (http://g.raphaeljs.com)
 * Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license.
 */

Raphael.fn.g.dotchart = function (x, y, width, height, valuesx, valuesy, size, opts) {
    function drawAxis(ax) {
        +ax[0] && (ax[0] = paper.g.axis(x + gutter, y + gutter, width - 2 * gutter, minx, maxx, opts.axisxstep || Math.floor((width - 2 * gutter) / 20), 2, opts.axisxlabels || null, opts.axisxtype || "t"));
        +ax[1] && (ax[1] = paper.g.axis(x + width - gutter, y + height - gutter, height - 2 * gutter, miny, maxy, opts.axisystep || Math.floor((height - 2 * gutter) / 20), 3, opts.axisylabels || null, opts.axisytype || "t"));
        +ax[2] && (ax[2] = paper.g.axis(x + gutter, y + height - gutter + maxR, width - 2 * gutter, minx, maxx, opts.axisxstep || Math.floor((width - 2 * gutter) / 20), 0, opts.axisxlabels || null, opts.axisxtype || "t"));
        +ax[3] && (ax[3] = paper.g.axis(x + gutter - maxR, y + height - gutter, height - 2 * gutter, miny, maxy, opts.axisystep || Math.floor((height - 2 * gutter) / 20), 1, opts.axisylabels || null, opts.axisytype || "t"));
    }

    opts = opts || {};
    var xdim = this.g.snapEnds(Math.min.apply(Math, valuesx), Math.max.apply(Math, valuesx), valuesx.length - 1),
            minx = xdim.from,
            maxx = xdim.to,
            gutter = opts.gutter || 10,
            ydim = this.g.snapEnds(Math.min.apply(Math, valuesy), Math.max.apply(Math, valuesy), valuesy.length - 1),
            miny = ydim.from,
            maxy = ydim.to,
            len = Math.max(valuesx.length, valuesy.length, size.length),
            symbol = this.g.markers[opts.symbol] || "disc",
            res = this.set(),
            series = this.set(),
            max = opts.max || 100,
            top = Math.max.apply(Math, size),
            R = [],
            paper = this,
            k = Math.sqrt(top / Math.PI) * 2 / max;

    for (var i = 0; i < len; i++) {
        R[i] = Math.min(Math.sqrt(size[i] / Math.PI) * 2 / k, max);
    }
    gutter = Math.max.apply(Math, R.concat(gutter));
    var axis = this.set(),
            maxR = Math.max.apply(Math, R);
    if (opts.axis) {
        var ax = (opts.axis + "").split(/[,\s]+/);
        drawAxis(ax);
        var g = [], b = [];
        for (var i = 0, ii = ax.length; i < ii; i++) {
            var bb = ax[i].all ? ax[i].all.getBBox()[["height", "width"][i % 2]] : 0;
            g[i] = bb + gutter;
            b[i] = bb;
        }
        gutter = Math.max.apply(Math, g.concat(gutter));
        for (var i = 0, ii = ax.length; i < ii; i++) if (ax[i].all) {
            ax[i].remove();
            ax[i] = 1;
        }
        drawAxis(ax);
        for (var i = 0, ii = ax.length; i < ii; i++) if (ax[i].all) {
            axis.push(ax[i].all);
        }
        res.axis = axis;
    }
    var kx = (width - gutter * 2) / ((maxx - minx) || 1),
            ky = (height - gutter * 2) / ((maxy - miny) || 1);
    for (var i = 0, ii = valuesy.length; i < ii; i++) {
        var sym = this.raphael.isArray(symbol) ? symbol[i] : symbol,
                X = x + gutter + (valuesx[i] - minx) * kx,
                Y = y + height - gutter - (valuesy[i] - miny) * ky;
        sym && R[i] && series.push(this.g[sym](X, Y, R[i]).attr({fill: opts.heat ? this.g.colorValue(R[i], maxR) : Raphael.fn.g.colors[0], "fill-opacity": opts.opacity ? R[i] / max : 1, stroke: "none"}));
    }
    var covers = this.set();
    for (var i = 0, ii = valuesy.length; i < ii; i++) {
        var X = x + gutter + (valuesx[i] - minx) * kx,
                Y = y + height - gutter - (valuesy[i] - miny) * ky;
        covers.push(this.circle(X, Y, maxR).attr({fill: "#000", stroke: "none", opacity: 0}));
        opts.href && opts.href[i] && covers[i].attr({href: opts.href[i]});
        covers[i].r = +R[i].toFixed(3);
        covers[i].x = +X.toFixed(3);
        covers[i].y = +Y.toFixed(3);
        covers[i].X = valuesx[i];
        covers[i].Y = valuesy[i];
        covers[i].value = size[i] || 0;
        covers[i].dot = series[i];
    }
    res.covers = covers;
    res.series = series;
    res.push(series, axis, covers);
    res.hover = function (fin, fout) {
        covers.mouseover(fin).mouseout(fout);
        return this;
    };
    res.click = function (f) {
        covers.click(f);
        return this;
    };
    res.href = function (map) {
        var cover;
        for (var i = covers.length; i--;) {
            cover = covers[i];
            if (cover.X == map.x && cover.Y == map.y && cover.value == map.value) {
                cover.attr({href: map.href});
            }
        }
    };
    return res;
};
;
/* END /2static/script/lib/graphael/g.dot.js */
/* START /2static/script/fecru/raphaelCharts.js */
window.FECRU = window.FECRU || {};
FECRU.RAPHAELCHARTS = {};

(function () {

    var LINE_COLOURS_PIE = ["#478ec7","#769810","#d8561f","#d7e52e","#0c4383","#5fbe41","#f5832b","#edef00","#0c87c9","#ad2a15"];
    var LINE_COLOURS_XY = ["#d7561f","#478ec7","#769810","#dee439","#5fbe41","#0c4383","#f5832b","#edef00","#0c87c9","#ad2a15","#aebf47"];
    var REPOSITORY_COLOURS = ["#d7561f","#769810","#dee439","#5fbe41","#0c4383","#f5832b","#edef00","#0c87c9","#ad2a15","#aebf47"];

    var months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];

    var getClr = function(i) {
        return LINE_COLOURS_XY[i % LINE_COLOURS_XY.length];
    };

    function getMinY(bars) {
        var minY = bars[0].y;
        for (var i = 1, ii = bars.length; i < ii; i++) {
            minY = Math.min(bars[i].y, minY);
        }
        return minY;
    }

    function getMaxY(bars) {
        var maxY = 0;
        for (var i = 0, ii = bars.length; i < ii; i++) {
            maxY = Math.max(bars[i].y + bars[i].h, maxY);
        }
        return maxY;
    }

    function toComp(num) {
        var prefix = ["",,, "K",,, "M",,, "G",,, "T",,, "P",,, "E",,, "Z",,, "Y"];
        for (var i = 0, ii = prefix.length; i < ii; i++) {
            if (typeof prefix[i] == "string") {
                if (num < Math.pow(10, i + 3)) {
                    return Math.round(num / Math.pow(10, i)) + prefix[i];
                }
            }
        }
        return Math.round(num / Math.pow(10, i - 1)) + prefix[prefix.length - 1];
    }

    function initDiv(divName) {
        AJS.$("body").append('<div id="' + divName + '-child" style="display:block;position:absolute; left:-4000px;">&nbsp;</div>');
        return divName + '-child';
    }

    function finaliseDiv(divName) {
        var div = AJS.$('#' + divName + '-child');
        div.removeAttr("style");

        //div.insertAfter(AJS.$('#' + divName));
        AJS.$('#' + divName).replaceWith(div);

    }

    function drawLocCharts(xValuesLoc, yValuesLoc, seriesLabelsShort, seriesLabelsLong, pageUrl) {

        var txt = {"text-anchor": "start", "font-size": 11, fill: "#666"};
        var popup;
        var dateText;

        var formattedDates = [];
        for (var i = 0, ii = xValuesLoc.length; i < ii; i++) {
            formattedDates[i] = formatDate(xValuesLoc[i]);
        }

        function hin2() {
            axisSet.hide();
            if (popup) {
                popup.remove();
                tooltipSet.hide();
                popup = null;
            }
            if (dateText) {
                dateText.remove();
                dateText = null;
            }
            for (var i = 0, ii = this.values.length; i < ii; i++) {
                tooltipSet[i].attr({text: seriesLabelsShort[i] + ": " + toComp(this.values[i])});
            }
            tooltipSet.show();
            if (this.line) {
                this.line.show();
            } else {
                this.line = rMain.set();
                this.line.push(rMain.path({opacity: .3}, "M" + [Math.round(this.x) + .5, 10, "l", 0, 130]).insertBefore(this));
                for (var j = 0, jj = this.values.length; j < jj; j++) {
                    this.line.push(rMain.circle(Math.round(this.x) + .5, this.y[j], 3).insertBefore(this).attr({fill: getClr(j), stroke: "#fff"}));
                }
            }
            popup = rMain.set();
            var maxy = Math.max.apply(0, this.y);
            var miny = Math.min.apply(0, this.y);
            popup.push(rMain.g.popupit(this.x, miny + (maxy - miny) / 2, tooltipSet, (this.x < 200) * 2 + 1).insertBefore(this).attr({fill: "#fff", stroke: "#666"}));
            tooltipSet.insertBefore(this);
            dateText = rMain.text(this.x, 150, (formattedDates[this.xIndex]));
        }

        function hout() {
            if (popup) {
                popup.remove();
                tooltipSet.hide();
                axisSet.show();
                popup = null;
                this.line.hide();
            }
            if (dateText) {
                dateText.remove();
                dateText = null;
            }
        }

        function fclick() {
            if (pageUrl) {
                var oldMaxDate = pageUrl.match(/maxDate=.*?(?=&|$)/);
                var newMaxDate = 'maxDate=' + xValuesLoc[this.xIndex];
                if (oldMaxDate) {
                    pageUrl = pageUrl.replace(oldMaxDate, newMaxDate);
                } else {
                    pageUrl += (pageUrl.indexOf("?") == -1) ? "?" : "&";
                    pageUrl += newMaxDate;
                }
                window.location = pageUrl;
            }
        }

        // draw sparkline

        var divSparkline = "locChartSparkline";
        var divSparklineChild = initDiv(divSparkline);

        var rSparkline = Raphael(divSparklineChild, 280, 37);
        rSparkline.g.linechart(0, 0, 280, 32, xValuesLoc, yValuesLoc, {colors: LINE_COLOURS_XY, width: 2});


        // draw main chart

        var divMain = "locChartMain";
        var divMainChild = initDiv(divMain);

        var bump = 20;
        var mainHeight = 150 + 10 + 20 * seriesLabelsLong.length;

        var rMain = Raphael(divMainChild, 280, mainHeight);

        // set up tooltip text
        var tooltipSet = rMain.set();
        for (var l = 0, ll = seriesLabelsLong.length; l < ll; l++) {
            tooltipSet.push(rMain.text(100, 30 + l * 15, "").attr({fill: getClr(l), "font-weight": 800}));
        }
        tooltipSet.hide();

        // draw legend
        for (var i = 0, ii = seriesLabelsLong.length; i < ii; i++) {
            rMain.g.disc(20, 150 + 20 * i + bump, 7).attr({stroke: "none", fill: getClr(i)});
            rMain.text(30, 150 + 20 * i + bump, seriesLabelsLong[i]).attr({"text-anchor": "start"});
        }

        rMain.path({opacity: .3}, "M" + [0, 130, "l", 276, 0]);

        // draw date axis
        var axisSet = rMain.set();

        var step = (276 - 9) / xValuesLoc.length;

        var dateWidth = 60;
        var lastX = - dateWidth / 2;

        for (var i = 0, ii = xValuesLoc.length; i < ii; i++) {

            var xVal = 12 + i * step;
            if (xVal - lastX > dateWidth) {
                lastX = xVal;
                //                axisSet.push(rMain.path({opacity: .3}, "M" + [xVal, 140, "l", 0, 5]));
                axisSet.push(rMain.text(xVal, 150, (formattedDates[i])));
            }
        }

        rMain.g.linechart(0, 10, 276, 130, xValuesLoc, yValuesLoc, {colors: LINE_COLOURS_XY, width: 2}).hoverColumn(hin2, hout).clickColumn(fclick);

        finaliseDiv(divSparkline);
        finaliseDiv(divMain);

    }

    function formatDate(ms) {

        var d = new Date(ms);

        var s = "";
        if (d.getDate() < 10) {
            s += '0';
        }
        s += (d.getDate()) + " ";

        s += months[d.getMonth()] + " ";

        var year = d.getYear() - 100;
        if (year < 10) {
            s += '0';
        }
        s += year;

        return s;
    }

    function drawCommitCharts(seriesLabels, sparklineLabels, sparklineData, commitsByDay, commitsByHour,
                              activityCalendarYears, activityCalendarData) {

        var dayLabels = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
        var hourLabels = ["00","01","02","03","04","05","06","07","08","09","10","11","12","13","14","15","16","17","18","19","20","21","22","23"];
        var monthLabels = ["J","F","M","A","M","J","J","A","S","O","N","D"];

        var divSparklineName = "sparklineSmall";
        var divSparklineChild = initDiv(divSparklineName);

        var rSmall = Raphael(divSparklineChild, 276, 32);
        rSmall.g.barchart(0, 0, 276, 32, sparklineData, false, {stacked: 1, vgutter: 0, colors: LINE_COLOURS_XY});
        rSmall.path({opacity: .3}, "M" + [0, 32, "l", 276, 0]);

        var divMainName = "mainCanvas";
        var divMainChild = initDiv(divMainName);
        var calendarHeight = 80 + 13 * activityCalendarYears.length;
        var mainHeight = 310 + calendarHeight;

        var r = Raphael(divMainChild, 300, mainHeight);

        // intialise tooltips
        var tooltipSet = r.set();
        for (var l = 0; l < seriesLabels.length; l++) {
            tooltipSet.push(r.text(100, 30 + l * 15, "").attr({fill: getClr(l), "font-weight": 800}));
        }
        tooltipSet.hide();

        var txt = {"text-anchor": "start", "font-size": 11, fill: "#666"};
        r.text(10, 10, "52 week commits volume").attr(txt);
        r.text(10, 110, "Commits by day").attr(txt);
        r.text(10, 200, "Commits by hour").attr(txt);
        r.text(10, 290, "Commit calendar").attr(txt);

        var hin = function() {
            for (var i = 0, ii = this.bars.length; i < ii; i++) {
                tooltipSet[i].attr({text: seriesLabels[i] + ": " + this.bars[i].value});
            }
            tooltipSet.show();
            this.popup = r.g.popupit(this.bars[0].x, getMinY(this.bars), tooltipSet, 2).insertBefore(this).attr({fill: "#fff", stroke: "#666"});
            tooltipSet.insertBefore(this);
        };

        var hin_below = function() {
            for (var i = 0, ii = this.bars.length; i < ii; i++) {
                tooltipSet[i].attr({text:  seriesLabels[i] + ": " + this.bars[i].value});
            }
            tooltipSet.show();
            this.popup = r.g.popupit(this.bars[0].x, getMaxY(this.bars), tooltipSet, 0).insertBefore(this).attr({fill: "#fff", stroke: "#666"});
            tooltipSet.insertBefore(this);
        };

        var hout = function() {
            if (this.popup) {
                this.popup.remove();
                tooltipSet.hide();
                delete this.popup;
            }
        };


        r.g.barchart(0, 20, 276, 70, sparklineData, false, {stacked: 1, vgutter: 0, colors: LINE_COLOURS_XY});
        r.path({opacity: .3}, "M" + [0, 90, "l", 276, 0]);

        r.g.barchart(20, 110, 235, 70, commitsByDay, false, {stacked: 1, vgutter: 15, colors: LINE_COLOURS_XY, "font-size": 11, fill: "#666"}).label(dayLabels).hoverColumn(hin, hout);

        r.g.barchart(25, 200, 230, 70, commitsByHour, false, {stacked: 1, vgutter: 15, colors: LINE_COLOURS_XY, "font-size": 11, fill: "#666"}).label(hourLabels).hoverColumn(hin, hout);


        var data = [],
                xs = [],
                ys = [];

        for (var i = 0, ii = activityCalendarData.length; i < ii; i++) {
            for (var j = 0, jj = activityCalendarData[j].length; j < jj; j++) {
                xs.push(j);
                ys.push(ii - i);
                data.push(activityCalendarData[i][j]);
            }
        }

        r.g.dotchart(0, 300, 270, calendarHeight, xs, ys, data, {symbol: "o", max: 6, heat: true, axis: "0 0 1 1", axisxstep: monthLabels.length - 1, axisystep: activityCalendarYears.length - 1, axisxlabels: monthLabels, axisxtype: " ", axisytype: " ", axisylabels: activityCalendarYears.reverse()}).hover(function () {
            this.tag = this.tag || r.g.tag(this.x, this.y, this.value, 0, this.r + 2).insertBefore(this);
            this.tag.show();
        }, function () {
            this.tag && this.tag.hide();
        }).axis.attr({"font-size": 10, fill: "#666"});

        finaliseDiv(divMainName);
        finaliseDiv(divSparklineName);
    }

    FECRU.RAPHAELCHARTS.commitSparkline = function(divName, width, height, queryParams, contextPath) {

        AJS.$.getJSON(contextPath + '/fe/commitSparkline.do', queryParams,
                function(_52) {
                    var r = Raphael(divName, width, height);

                    r.g.barchart(0, 10, width, height - 10, _52.data, false, {stacked: 1, vgutter: 0, colors: LINE_COLOURS_XY});
                });

    };

    FECRU.RAPHAELCHARTS.sidebarCharts = function(data, url) {
        drawLocCharts(data.locDataX, data.locDataY, data.seriesLabelsShort, data.seriesLabelsLong, url);
        drawCommitCharts(data.seriesLabelsShort, data.sparklineLabels, data.sparklineData, data.commitsByDay, data.commitsByHour, data.activityCalendarYears, data.activityCalendarData);
    };

    FECRU.RAPHAELCHARTS.sidebarChartsAjax = function(queryParams, contextPath, url) {

        AJS.$.getJSON(contextPath + '/fe/sidebarChartsJson.do', queryParams,
                function(data) {
                    FECRU.RAPHAELCHARTS.sidebarCharts(data, url);
                });
    };


})();

;
/* END /2static/script/fecru/raphaelCharts.js */
/* START /2static/script/fecru/navbuilder.js */
window.FECRU = window.FECRU || {};
FECRU.NAVBUILDER = FECRU.NAVBUILDER || {};

(function(namespace) {
    var EXTRA_ESCAPE_CHARS = /['\\(\\)]/g;
    function strictUriEncode(str) {
        return encodeURIComponent(str).replace(EXTRA_ESCAPE_CHARS, function(c) {
            switch(c) {
                case "'": return '%27';
                case "(": return '%28';
                case ")": return '%29';
            }
        });
    }

    /**
     * Generate a url for browsing the given repository at tag
     * @param repositoryName the repository name.
     * @param tag the tag name.
     * @return a string uri (includes the page context path) with the repository and tag component encoded
     */
    namespace.browseAtTag = function(repositoryName, tag) {
        return FECRU.pageContext + "/browse/~tag=" + strictUriEncode(tag) + "/" + strictUriEncode(repositoryName);
    };

    namespace.graphAtChangeset = function(repositoryName, csid) {
        return FECRU.pageContext + "/graph/" + strictUriEncode(repositoryName) + (csid ? "?csid="  + strictUriEncode(csid) : "");
    };

    namespace.user = function(userName) {
        return FECRU.pageContext + "/user/" + strictUriEncode(userName);
    };

    namespace.avatar = function(userName) {
        return FECRU.pageContext + "/avatar/" + strictUriEncode(userName);
    };

})(FECRU.NAVBUILDER);;
/* END /2static/script/fecru/navbuilder.js */
/* START /script/common/global.js */
//crucible global.js
var $dropdown	= 0;
var dropdownTimer = 0;

function openDropDown(id) {
    clearTimeout(dropdownTimer);

    // Close the old menu if it's not the same menu as is already open
    if ($dropdown && (id instanceof AJS.$ ? ($dropdown.get(0) !== id.get(0)) : ($dropdown.attr("id") !== id))) {
        $dropdown.fadeOut(100);
    }

    // get new menu and show it
    var $menu = (id instanceof AJS.$) ? id : AJS.$("#" + id);
    $dropdown = $menu.show();
}

function closeDropDown() {
    if ($dropdown && $dropdown.length === 1) {
        dropdownTimer = setTimeout(function(){
            $dropdown.fadeOut(100);
        }, 200);
    }
}

function simpleSwap(toHide, toShow) {
    if (typeof(toHide) == 'object') {
        AJS.$(toHide).hide();
    } else {
        AJS.$("#"+toHide).hide();
    }
    if (typeof(toShow) == 'object') {
        AJS.$(toShow).show();
    } else {
        AJS.$("#"+toShow).show();
    }
}
function expandAll(ids, prefix) {
    toggleAll(ids, true, false, prefix);
}
function collapseAll(ids, prefix) {
    toggleAll(ids, false, true, prefix);
}
function expandSelected(allIds, selectedIds, prefix) {
    //first collapse all
    toggleAll(allIds, false, true, prefix);
    toggleAll(selectedIds, true, false, prefix);
}
function toggleType(ids, prefix) {
    toggleAll(ids, false, false, prefix);
}
function toggleBasic(nodeName) {
    toggleNodeAndImage(nodeName, false, false);
}
function collapseBasic(nodeName) {
    toggleNodeAndImage(nodeName, false, true);
}
function expandBasic(nodeName) {
    toggleNodeAndImage(nodeName, true, false);
}
function toggleAll(ids, forceOpen, forceClose, prefix) {
    prefix = prefix || '';
    for (var i = 0; i < ids.length; i++) {
        var theNode = prefix + ids[i];
        toggleNodeAndImage(theNode, forceOpen, forceClose);
    }
    return false;
}

function toggleNodeAndImage(nodeName, forceOpen, forceClose) {
    if (!nodeName) {
        return;
    }
    nodeName = FECRU.sanitizeId(nodeName);
    var $node = AJS.$("#"+nodeName);
    if ($node.length === 0) {
        return;
    }

    var img = AJS.$("#"+nodeName+'img');
    if (img.length === 1) {
        var swapImage = true;
    }

    // Don't use is(":hidden") here, because we need to force things closed even if their parents are closed
    var shouldOpen = $node.css("display") === 'none';
    shouldOpen = (!forceClose) && (forceOpen || shouldOpen);
    if (shouldOpen) {
        $node.show();
        if (swapImage) {
            img.attr("src", FECRU.pageContext + '/' + FECRU.staticDirectory + '/images/arrow_open.gif' );
        }
    } else {
        $node.hide();
        if (swapImage) {
            img.attr("src", FECRU.pageContext + '/' + FECRU.staticDirectory + '/images/arrow_closed.gif' );
        }
    }
}

function rollover(obj) {
    if (obj.tagName == 'IMG') {
        var imgsrc = obj.src.replace(/\.gif$/, '');
        obj.src = imgsrc + '_over.gif';
    }
    return false;
}

function rollout(obj) {
    if (obj.tagName == 'IMG') {
        obj.src = obj.src.replace(/\_over\.gif$/, '.gif');
    }
    return false;
}

function toggleOverflow(handle, element) {
    var $el = AJS.$(element);
    var $hd = AJS.$(handle);
    if ($el.css("overflow") == 'hidden' || $el.css("overflow") == '') {
        $el.css({'overflow':'visible', 'height':'auto'});
        $hd.attr("src", $hd.attr("src").replace(/expand\.gif$/, 'collapse.gif') );
    } else {
        $el.css({'overflow':'hidden', 'height':'1.3em'});
        $hd.attr( "src", $hd.attr("src").replace(/collapse\.gif$/, 'expand.gif') );
    }
}


function submitDefaultForm(command) {
    document.defaultForm.command.value = command;
    document.defaultForm.submit();
}

function toggleWording(handle) {
    var $handleEl = AJS.$(handle);
    var handleText = $handleEl.html().substring(0, 4);
    switch (handleText) {
        case 'Show':
            $handleEl.html($handleEl.html().replace(/^Show/, 'Hide'));
            return true;
        case 'Hide':
            $handleEl.html($handleEl.html().replace(/^Hide/, 'Show'));
            return true;
        case 'Expa':
            $handleEl.html($handleEl.html().replace(/^Expand/, 'Collapse'));
            return true;
        case 'Coll':
            $handleEl.html($handleEl.html().replace(/^Collapse/, 'Expand'));
            return true;
        case 'More':
            $handleEl.html($handleEl.html().replace(/^More/, 'Less'));
            return true;
        case 'Less':
            $handleEl.html($handleEl.html().replace(/^Less/, 'More'));
            return true;
    }
    return false;
}

function show(toShow, bool) {
    AJS.$(toShow).toggle(bool);
}

function submitCruSearch(inputEl) {
    window.location = FECRU.pageContext + "/cru/search?query=" + encodeURIComponent(inputEl.value);
}

function searchReviews() {
    var searchVal = document.forms.searchForm["search.text"].value;
    if (searchVal) {
        window.location = FECRU.pageContext + "/cru/search?query=" + encodeURIComponent(searchVal);
    } else {
        window.location = FECRU.pageContext + "/cru/search";
    }
}
function searchComments() {
    var searchVal = document.forms.searchForm["query"].value;
    if (searchVal) {
        window.location = FECRU.pageContext + "/cru/commentSearch?search.text=" + encodeURIComponent(searchVal);
    } else {
        window.location = FECRU.pageContext + "/cru/commentSearch";
    }
}

var ovAnk = false; //tracks whether the mouse is over a "real" anchor
function toggleSensitively(id) {
    if (!ovAnk) {
        toggleBasic(id);
    }
}
;
/* END /script/common/global.js */
/* START /2static/script/lib/ajs/contentnamesearch.js */
if(!(/jwebunit/).test(navigator.userAgent.toLowerCase())){
    FECRU.UI.setupQuicksearch = function () {
        AJS.$("#quick-search-form").submit(function () {
            return AJS.$.trim(AJS.$("#quick-search-input").val()).length > 0;
        });

        var $quickInput = AJS.$("#quick-search-input");
        $quickInput.placeholder($quickInput.val());

        var attr = {
            cache_size: 30,
            max_length: 1,
            effect: "appear" // TODO: add "fade" effect
        };
        var dd,
            cache = {},
            cache_stack = [],
            timer;

        function regexEscape(text) {
            if (!arguments.callee.sRE) {
                var specials = [
                    '/', '.', '*', '+', '?', '|',
                    '(', ')', '[', ']', '{', '}', '\\'
                    ];
                arguments.callee.sRE = new RegExp(
                    '(\\' + specials.join('|\\') + ')', 'g'
                    );
            }
            return text.replace(arguments.callee.sRE, '\\$1');
        }

        var hider = function (list) {
             AJS.$("a span", list).each(function () {
                var $a = AJS.$(this),
                    elpss = AJS("var", "&#8230;"),
                    elwidth = elpss[0].offsetWidth,
                    width = this.parentNode.parentNode.parentNode.parentNode.offsetWidth,
                    isLong = false,
                    rightPadding = 20; // add some padding so the ellipsis doesn't run over the edge of the box

                    // get the hidden space name property from the span
                    var spaceName = AJS.dropDown.getAdditionalPropertyValue($a, "spaceName");
                    if (spaceName != null) {
                        spaceName = "(" + spaceName + ") ";
                    } else {
                        spaceName = "";
                    }
                    AJS.dropDown.removeAllAdditionalProperties($a);

                this.realhtml = this.realhtml || $a.html();
                $a.html("<em>" + this.realhtml + "</em>");
                $a.append(elpss);
                //$a.attr("title", spaceName + this.realhtml.replace(/<\/?strong>/gi, ""));
                this.elpss = elpss;

                AJS.$("em", $a).each(function () {
                    var $label = AJS.$(this);

                    $label.show();
                    if (this.offsetLeft + this.offsetWidth + elwidth > width - rightPadding) {

                        var childNodes = this.childNodes;
                        var success = false;

                        for (var j = childNodes.length - 1; j >= 0; j--) {
                            var childNode = childNodes[j];
                            var truncatedChars = 1;

                            var valueAttr = (childNode.nodeType == 3) ? "nodeValue" : "innerHTML";
                            var nodeText = childNode[valueAttr];

                            do {
                                if (truncatedChars <= nodeText.length) {
                                    childNode[valueAttr] = nodeText.substr(0, nodeText.length - truncatedChars++);
                                } else { // if we cannot fit even one character of the next word, then try truncating the node just previous to this
                                    break;
                                }
                            } while (this.offsetLeft + this.offsetWidth + elwidth > width - rightPadding);

                            if (truncatedChars <= nodeText.length) {
                                // we've managed truncate part of the word and fit it in
                                success = true;
                                break;
                            }
                        }

                        if (success) {
                            isLong = true;
                        } else {
                            $label.hide();
                        }
                    }
                });
                elpss[isLong ? "show" : "hide"]();
            });
        };

        var searchBox = AJS.$("#quick-search-input");
        var $ddContainer = AJS.$("#quick-search-dropdown-holder");
        if (AJS.$.browser.safari && searchBox.length === 1) {
        //    searchBox[0].type = "search";
            searchBox[0].setAttribute("results", 10);
            searchBox[0].setAttribute("placeholder", "search");
        }
        var jsonparser = function (json, resultStatus) {
            var hasErrors = json.statusMessage ? true : false; // right now, we are overloading the existance of a status message to imply an error
            var matches = hasErrors ? [[{html: json.statusMessage, className: "error"}]] : json.matches;

            if (!hasErrors) {
                var query = json.query;
                if (!cache[query + FECRU.quickSearchUrl] && resultStatus == "success") {
                    cache[query + FECRU.quickSearchUrl] = json;
                    cache_stack.push(query + FECRU.quickSearchUrl);
                    if (cache_stack.length > attr.cache_size) {
                        delete cache[cache_stack.shift() + FECRU.quickSearchUrl];
                    }
                }
            }

            // do not update drop down for earlier requests. We are only interested in displaying the results for the most current search
            if (json.query != searchBox.val() && json.query.charAt(json.query.length-1) != "*") {
                return;
            }

            var old_dd = dd;
            dd = AJS.dropDown(matches, {selectionHandler: function (e, selected) {
                if (selected) {
                    var setHref = function(selected, href) {
                        if (href == "#" || !href) {
                            // if this menu item doesn't simply send us to a new url, trigger a custom event on it.
                            AJS.$(selected).trigger("menuItemSelected");
                        } else {
                            window.location = href;
                        }
                    };

                    var link = selected;
                    if (link.get(0).nodeName.toLowerCase() !== "a") {
                        link = selected.find("a");
                    }
                    setHref(link, link.attr("href"));
                    e.preventDefault();
                    return false;
                }
            }})[0];
            dd.$.attr("id", "quick-nav-drop-down");
            dd.$.appendTo($ddContainer);

            dd.onhide = function (causer) {
                if (causer == "escape") {
                    searchBox.focus();
                }
            };
            if (json.queryTokens) {
                var spans = AJS.$("span", dd.$);
                for (var i = 0, ii = spans.length - 1; i < ii; i++) {
                    (function () {
                        var $this = AJS.$(this),
                                html = $this.html();
                        // highlight matching tokens
                        var match = "(";
                        for (var j = 0, jj = json.queryTokens.length - 1; j <= jj; j++) {
                            match += regexEscape(json.queryTokens[j]);
                            if (j < jj) {
                                match += "|";
                            }
                        }
                        match += ")";
                        html = html.replace(new RegExp(match, "gi"), "<strong>$1</strong>");
                        $this.html(html);
                    }).call(spans[i]);
                }
            }
            hider(dd.$);
            if(typeof onShow == "function") {
                onShow.apply(dd);
            }
            dd.hider = function () {
                hider(dd.$);
            };
            AJS.onTextResize(dd.hider);
            if (old_dd) {
                dd.show();
                dd.method = attr.effect;
                AJS.unbindTextResize(old_dd.hider);
                old_dd.$.remove();
            } else {
                dd.show(attr.effect);
            }
            AJS.$("#busyQnav").hide();
        };
        searchBox.data("oldval", searchBox.val());
        var keyUpHandler = function () {
            var val = searchBox.val();

            var oldVal = searchBox.data("oldval");

            if (val != oldVal) {
                searchBox.data("oldval", val);
                if (!searchBox.hasClass("placeholded")) {
                    clearTimeout(timer);

                    if ((/[\S]{2,}/).test(val)) {
                        if (cache[val + FECRU.quickSearchUrl]) {
                            jsonparser(cache[val + FECRU.quickSearchUrl]);
                        } else {
                            var url = FECRU.quickSearchUrl + "q=" + AJS.escape(val) + "&type=ajax&searchAllDirs=false";
                            timer = setTimeout(function () { // delay sending a request to give the user a chance to finish typing their search term(s)'
                                AJS.$("#busyQnav").show();
                                return AJS.$.ajax({
                                    type: "GET",
                                    url: url,
                                    data: null,
                                    success: jsonparser,
                                    dataType: "json",
                                    timeout: 20000,
                                    error: function ( xml, status, e ) { // ajax error handler
                                        if (status == "timeout") {
                                            jsonparser({statusMessage: "Query has timed out", query: val}, status);
                                        } else if (status == "error") {
                                            jsonparser({statusMessage: "A server-side error has occurred.", query: val}, status);
                                        }
                                    }
                                });
                            }, 600);
                        }
                    } else {
                        AJS.$("#busyQnav").hide();
                        dd && dd.hide();
                    }
                }
            }
        };
        searchBox.keyup(keyUpHandler);
    };
}
;
/* END /2static/script/lib/ajs/contentnamesearch.js */
/* START /2static/script/fe/fisheye-ui.js */
window.FE = window.FE || {};
(function() {
    /**
     * Converts an id to a jquery object.
     *
     * @param el an element id, a jquery id selector (starting with '#'), or a jquery elem
     * @return an object
     */
    var ensureObject = function (el) {
        if (typeof el == "string") {
            if (el.charAt(0) != "#") {
                el = '#' + el;
            }
            el = AJS.$(el);
        }

        return el;
    };

    /**
     * Returns the minimum height of an element (el) so that it is always at least at
     * the height of its parent element, and optionally takes into account the height/s of
     * any siblings when el is not specified as onlyChild.
     *
     * onlyChild should be used either where el has no sibling element, or where you want
     * el to not consider its siblings - where el and siblings might be floated, for example.
     *
     */
    var elementGetMinHeight = function (el, onlyChild) {
        var element = ensureObject(el);
        if (element.length === 0) {// If element doesn't exist on the page a length of 0 will be returned
            return 0
        }

        var parentHeight = AJS.$(window).height(),
            siblingsHeight = 0
        ;

        if (!onlyChild) {
            var siblings = element.siblings(),
                siblingsCount = siblings.size()
            ;

            for (var i = 0; i < siblingsCount; i += 1) {
                var sibling = siblings.eq(i);
                if (sibling.is(":visible")) {
                    siblingsHeight += sibling.outerHeight(true);
                }
            }
        }

        return parseInt(parentHeight, 10) - parseInt(siblingsHeight, 10);
    };
    FE.elementGetMinHeight = elementGetMinHeight;

    /**
     * Sets the minimum height of an element (el) to "height" and works hand in hand with
     * elementGetMinHeight.
     *
     * Rather than being part of a larger function which might set _and_ get, this usage
     * allows other functions to utilize the method, without having to determine el's parent and/or
     * sibling elements.
     *
     */
    var elementSetMinHeight = function (el, height) {
        var element = ensureObject(el);

        element.css({
            minHeight: outerHeightProperties(el, height)
        })
    };

    /**
     * Sets the height of an element (el) to "height" and works hand in hand with
     * elementGetMinHeight.
     *
     * Rather than being part of a larger function which might set _and_ get, this usage
     * allows other functions to utilize the method, without having to determine el's parent and/or
     * sibling elements.
     *
     */
    var elementSetHeight = function (el, height) {
        var element = ensureObject(el);

        element.height(outerHeightProperties(el, height))
    };

    /**
     * Determines whether an element (el) has style proprties that affect its outerHeight(true) value
     *
     * You can't set outerHeight so we need to take outerHeight effecting properties in to account, so we
     * accept a height parameter, test for any "outer" properties, and subtract any we find from the height given
     *
     */

    var outerHeightProperties = function (el, height) {
        var element = ensureObject(el),
            outerProperties = ["borderTopWidth", "borderBottomWidth", "marginTop", "marginBottom", "paddingTop", "paddingBottom"],
            properties = 0
        ;

        for (var i = 0, l = outerProperties.length; i < l; i += 1) {
            var property = parseInt(element.css(outerProperties[i]), 10);

            if (property) {
                properties += property;
            }
        }

        return height - properties;
    };
    FE.outerHeightProperties = outerHeightProperties;

    var panelsSetMinHeight = function () {
        if (AJS.$("#atlas").length > 0) {//makes that we only try to force the element sizing on pages that can benefit from it
            if (AJS.$("#columns").length > 0) {//threePanelPAgeContent.tag used
                elementSetHeight("columns", elementGetMinHeight("#atlas", false)
                       - AJS.$('#content-fixed').outerHeight(true)
                );

                // Using the specified height, not the computed height (which is what css('height') returns)
                // So we have to go native (jQuery doesn't have a way to get the actual style for the element).
                var columnsEl = document.getElementById("columns"),
                    columnHeight = parseInt(columnsEl.style.height, 10) // not .height()
                        - AJS.$("#content-sidebar-head").outerHeight() // Less the branch selector
                        - 2; // Less 2 pixels (from a border i think. Its cheaper hard coding this

                // If we are on the annotation/diff view, restrict, otherwise let it expand
                if (FECRU.restrictToWindowHeight) {
                    elementSetHeight("content-navigation-panel", columnHeight);
                    elementSetHeight("column-content", columnHeight);
                } else {
                    elementSetMinHeight("content-navigation-panel", columnHeight);
                }
            } else if (AJS.$("#content-single").length > 0) {
                elementSetMinHeight("content-single", AJS.$(window).height()
                       - AJS.$('#header').outerHeight(true)
                       - AJS.$('#footer').outerHeight(true)
                );
            }
            AJS.trigger('fe-page-resize');
        }
    };

    /**
     * Bind a callback that fires when resizing is completed on matching elements.
     *
     * A resize is considered complete after completionDelay ms have passed since
     * the last resize event.
     *
     * This prevents firing the resize event handler multiple times before the resize
     * is actually complete.
     *
     * This should only be being utilised by Snippets pages and TODO deprecate in favour
     * of elementSetMinHeight
    */

    var hasSetupPageFillHeight = false;
    FE.setupPageFillHeight = function ($changeHeight, useMinHeight) {
        if (hasSetupPageFillHeight) {
            return;
        }

        var offset = ($changeHeight) ? $changeHeight.offset().top : 34;

        $changeHeight = $changeHeight || AJS.$("#atlas");
        hasSetupPageFillHeight = true;
        var onResize = function() {
            var windowHeight = AJS.$(window).height(),
                footerHeight = AJS.$("#footer").outerHeight(),
                borderAndPadding = $changeHeight.outerHeight() - $changeHeight.height();
            $changeHeight.css(useMinHeight ? 'min-height' : 'height', windowHeight - footerHeight - offset - borderAndPadding);
        };
        onResize();
        FECRU.UI.setCompletedResizeTimeout(window, onResize);
    };

    var columnResize = function (min) {
        AJS.$("#content-resizable").resizable({
            ghost: true,
            handles: "e",
            maxWidth: 600,
            minWidth: min || 310,
            start: function() {
                AJS.$(".tearout-tabs").append(AJS.$(document.createElement("div")).addClass("ui-resizable-helper"));
            },
            stop: function() {
                AJS.$(".tearout-tabs").children(".ui-resizable-helper").remove();
                AJS.$('#content-sidebar').css('width', AJS.$(this).css('width'));
                AJS.trigger("sidebar-resize");
            }
        });

        // This is being done on mousedown/mouseup instead of resizable.start/stop because of
        // strange behavior in Chrome (possibly others). The cursor switches to text during resize
        // instead of remaining e-resize. I don't know why.
        var selector = "#content-resizable .ui-resizable-handle, #content-shield, .shielded";
        AJS.$(document).delegate(selector, "mousedown", function() {
            AJS.$("html").addClass("shielded");
        }).delegate(selector, "mouseup", function() {
            AJS.$("html").removeClass("shielded");
        });
    };

    var setupWatch = function() {
        var $watchform = AJS.$("#watchform");
        var eventName = "click.watchform";
        if ($watchform.length > 0) {
            var $input = $watchform.children("input");
            AJS.$(".watch-on", $watchform).unbind(eventName).bind(eventName, function(evnt) {
                evnt.preventDefault();
                $input.val('off');
                $watchform.submit();
            });
            AJS.$(".watch-off", $watchform).unbind(eventName).bind(eventName, function(evnt) {
                evnt.preventDefault();
                $input.val('on');
                $watchform.submit();
            });
        }
    };

    var reloadFilePaneAndHeader = function(href) {
        var origUrl = AJS.$("#origUrl").val();
        var $spinner = AJS.$("#file-table-spinner");
        $spinner.show();
        FECRU.AJAX.ajaxDo(FECRU.pageContext + "/json/fe/loadFilePane.do", {href: href, origUrl: origUrl}, function(resp) {
            if (resp.worked) {
                var replacement = AJS.$.clean([resp.fileTable], document);
                AJS.$("#browse-table").replaceWith(replacement);
                AJS.$("#masthead").replaceWith(resp.mastheadDiv);
                AJS.$("#dirlist-toolbar-bar").replaceWith(resp.taskBarBar);
                AJS.$(".content-view").replaceWith(resp.contentFixed);
                AJS.$("#content-fixed > .header").replaceWith(resp.contentTitle);

                var encodedLinkUrl = resp.encodedBranchSelectorUrl;
                AJS.$("#select-branch-all, #select-branch-default, #select-branch-last")
                    .attr('href', function(index, oldHref) {
                        this.href = encodedLinkUrl + oldHref.substring(oldHref.indexOf('?'));
                    });

                AJS.$('#branch-constraint-form').attr('action', encodedLinkUrl);

                setupWatch();
                FECRU.RSS.setupRSSDialog();
                FECRU.UI.setupQuicksearch();
            }
            $spinner.hide();
        }, false);
    };

    /**
     * Find a link in the directory tree by its href
     */
    var $findLink = function(href) {
        return AJS.$("#navigation-tree").find("a.pathLink[href='" + href + "']");
    };

    FE.browseDirectoryPathLinkFunction = function(event) {
        var $node = AJS.$(event.target);
        var href = $node.attr("href");
        var toggled = function () { };
        var self = FE.browseDirectoryPathLinkFunction;
        if ($node.hasClass("browse-directory")) {
            // find the node in the tree with the same href as us
            var $selectedLink = $findLink(href);
            FECRU.BROWSE.selectLink($selectedLink, toggled, self);
        } else {
            FECRU.BROWSE.selectLink($node, toggled, self);
        }
        reloadFilePaneAndHeader(href);
        return false;
    };

    FE.extractRevisionDetailsSortKeys = function(cell) {
        var $node = AJS.$(cell);
        var $input = $node.children("input");
        if ($input.size() > 0) {
            return $input.val();
        } else {
            var result = AJS.$.trim($node.text());
            if ($node.hasClass("browse-comment")) {
                // sort directories before files
                if ($node.children("a").hasClass("browse-directory")) {
                    result = "directory-" + result;
                } else {
                    result = "file-" + result;
                }
            }
            return result;
        }
    };

    FE.setupTable = function(prefix, rowClickFn, extractionFn) {
        FE.resetupTable(prefix, extractionFn);
        FECRU.UI.tableRowClick(prefix, rowClickFn);
    };

    FE.resetupTable = function(prefix, extractionFn) {
        columnResize();
        FECRU.UI.tableSort(prefix, extractionFn);
    };

    FE.toggleTabs = function () {
        AJS.$(document).delegate(".tearout-tabs li", "click", function () {
            var active = AJS.$(this).hasClass("tearout-active") ? true : null;
            var tab = AJS.$(this).attr("class").split("-")[1];
            var panel = "#panel-" + tab;
            var content = AJS.$('#content');

            AJS.$(".tearout-tabs li").each(function () {
                AJS.$(this).removeClass("tearout-active");
                AJS.$(this).children("a").unbind();
            });

            AJS.$(".panel-tearout", "#content-navigation").each(function () {
                AJS.$(this).addClass("hidden");
            });
            // preference will be null if we are ignoring preferences
            var preference = AJS.$(this).children("input").val();
            if (active) {
                content.addClass('collapsed-sidebar');
                // hiding everything
                if (preference) {
                    FECRU.PREFS.setPreference("shp", "N");
                }
                AJS.trigger('sidebar-collapsed');
            } else {
                AJS.$(this).addClass("tearout-active");
                var wasCollapsed = content.hasClass('collapsed-sidebar');
                content.removeClass('collapsed-sidebar');
                AJS.$(panel).removeClass("hidden");
                if (preference) {
                    FECRU.PREFS.setPreferences({slp: preference, shp: "Y"});
                }
                wasCollapsed && AJS.trigger('sidebar-expanded');
            }
            // resize the pane when in changeset view
            if (AJS.$('#section-changeset-view').length > 0) {
                AJS.$(window).resize();
            }
        });
    };

    var hasSetupPanes = false;

    FE.setupPanes = function() {
        if (!hasSetupPanes) {
            columnResize(310);
            hasSetupPanes = true;
        }

        var $bottomToolbar = AJS.$("div.toolbar-bottom");

        if ($bottomToolbar.size() && $bottomToolbar.offset().top < AJS.$(window).height()) {
            $bottomToolbar.hide();
        }
    };

    FE.streamMoreFocus = function () {
        AJS.$(document).delegate("#stream a.more", "click", function(){
            AJS.$(this).closest(".stream").addClass("stream-focus");
            AJS.$("body").one("click",function(){
                AJS.$("#stream").find(".stream").removeClass("stream-focus");
            });
        });
    };

    FECRU.UI.refreshSearch = function(url) {
        var repoQuicksearchURL = FECRU.quickSearchUrl;
        FECRU.quickSearchUrl = url;
        var searchBox = AJS.$("#quick-search-input");
        searchBox.removeData("oldval");
        searchBox.trigger("keyup");
        FECRU.quickSearchUrl = repoQuicksearchURL;
        return false;
    };

    AJS.$(function () {
        // Give a warning if firebug is running
        FECRU.UI.warnAboutFirebug(function() {
            AJS.$(window).resize();
        });

        panelsSetMinHeight();
        FECRU.UI.setCompletedResizeTimeout(window, panelsSetMinHeight);

        //setup the watch links
        setupWatch();

        FE.setupPanes();

        if (!AJS.$.support.opacity) {
            FE.streamMoreFocus();
        }

        /* branch selector stuff */
        var showInputError = function() {
            FECRU.AJAX.appendErrorResponse("FishEye could not find a branch matching '" + $input.val() + "'");
            FECRU.AJAX.showErrorBox();
            $input.trigger("reset.autocomplete");
        };

        var triggerKeyDownOnInput = function(keyCode) {
            var keydown = jQuery.Event('keydown');
            keydown.which = keyCode;
            keydown.keyCode = keydown.which;
            $input.trigger(keydown);
        };

        var $input = AJS.$('#branchConstraint-input'),
            $branchSelector = AJS.$('#branch-selector'),
            $selectLastBranch = AJS.$('#select-branch'),
            $editLastBranch = $branchSelector.find('.edit-last-branch'),
            $closeLastBranch = $branchSelector.find('.close-last-branch'),
            $constraintForm = AJS.$('#branch-constraint-form'),
            $branchConstraint = $branchSelector.find('.branchConstraint'),
            $tagConstraint = $branchSelector.find('.tagConstraint')
        ;

        // if there is no last branch, we dont want the edit box to go away
        if ($editLastBranch.length === 1) {
            // if you click on the edit button, hide the edit button and show the autocomplete
            $editLastBranch.click(function() {
                $branchSelector.addClass('edit');
                $input.focus();
                if (AJS.$.browser.msie) {
                    $constraintForm.closest(".toolbar-group").siblings().attr("style", "");
                }
            });

            // hide and clear the autocomplete field and show the
            var hideAutoComplete = function() {
                $input.trigger("reset.autocomplete"); // clears it and handles internal ajax calls etc
                $branchSelector.removeClass('edit');
                FECRU.AJAX.stopSpin('branchConstraint-input'); // if it was mid-ajax call
                $input.trigger("placeholder.reset"); // so it shows the placeholder text
            };

            $input
                .keydown(function(e) {
                    if (e.keyCode === AJS.$.ui.keyCode.ESCAPE) {
                        hideAutoComplete();
                    }
                });
            $closeLastBranch.click(hideAutoComplete);

            // if we are already on the branch, clicking the branch button edits it
            if ($selectLastBranch.hasClass('active')) {
                $selectLastBranch.children('a').click(function(e) {
                    $editLastBranch.click();
                    e.preventDefault();
                    e.stopPropagation();
                });
            }

        }

        FECRU.UI.processSelectedBranch = function(data) {
            if (!data) {
                return;
            }
            triggerKeyDownOnInput(AJS.$.ui.keyCode.ESC); // hide the select box
            var type;

            if (data.isBranch) {
                $branchConstraint.val(data.id);
                $tagConstraint.val('');
                type = 'b';
            } else if (data.isTag) {
                $tagConstraint.val(data.id);
                $branchConstraint.val('');
                type = 't';
            } else {
                return;
            }

            FECRU.AJAX.startSpin(AJS.$('.close-last-branch'), 'selected-spinner', true);
            AJS.$('body').children('.ac_results').hide();
            CRU.UTIL.makeCssRule('.ac_results', 'display: none !important;'); // since the results may not exist, and
                                                                              // this page is getting reloaded anyway,
                                                                              // we create a css rule to prevent it
                                                                              // from being show again unnecessarily

            $input.val(data.id)
                .addClass('disabled')
                .siblings('.fecru-autocomplete-dropdown-icon')
                    .removeClass('visible');

            var cookieJson = {};
            var branchJson = {};
            branchJson['t'] = type;
            branchJson['n'] = data.id;
            cookieJson[FE.branchSelectorRepName] = branchJson;

            $branchSelector.find('#lastSelected').val(JSON.stringify(cookieJson)); // set the preference
            $constraintForm.submit();
        };

        var autocompleteEnterPressed = false; // enter was pressed before results came back

        $input
            .keydown(function(e) {
                if (e.keyCode === AJS.$.ui.keyCode.ENTER) {
                    e.preventDefault();
                    e.stopPropagation();

                    autocompleteEnterPressed = true;
                }
            })
            .change(function() {
                autocompleteEnterPressed = false;
            })
            .bind("autocomplete-data-received", function(event, result) {
                if (autocompleteEnterPressed) {
                    var inputValue = $input.val();
                    var dataList = result.data;
                    autocompleteEnterPressed = false;
                    if (dataList && dataList.length) {
                        if (dataList.length === 1) {
                            // select the one value that is there
                            triggerKeyDownOnInput(AJS.$.ui.keyCode.ENTER);
                        } else {
                            for(var i = 0, len = dataList.length; i < len; i++) {
                                var data = dataList[i].data;
                                if (data.id.toLowerCase() === inputValue.toLowerCase()) {
                                    // select the exact match
                                    FECRU.UI.processSelectedBranch(data);
                                }
                            }
                        }
                    } else {
                        if ($input.val()) {
                            showInputError();
                        }
                    }
                }
            });
    });
})();
;
/* END /2static/script/fe/fisheye-ui.js */
/* START /2static/script/fe/fisheye-article.js */
window.FE = window.FE || {};
FE.ARTICLE = (function(window, $) {


    function expandCommentText($container) {
        if (!$container.is(".article-board")) {
            $container = $container.find(".article-board");
        }
        $container
            .removeClass("article-terse")
            .addClass("article-verbose");
    };

    function collapseCommentText($container) {
        if (!$container.is(".article-board")) {
            $container = $container.find(".article-board");
        }
        $container
            .addClass("article-terse")
            .removeClass("article-verbose");
    };


    /**
     * Retreive the height of a child of the $articleMessage and store it in the $articleMessage data as a cache
     * @param $articleMessage container of the messages
     * @param messageComponent css class of the $articleMessage child, also used as the data key
     */
    function getHeightForArticleMessage($articleMessage, messageComponent) {
        var height = $articleMessage.data(messageComponent);
        if (height == null || height <= 0) {
            height = $articleMessage.children("." + messageComponent).outerHeight(false);
            $articleMessage.data(messageComponent, height);
        }
        return height;
    };

    function toggleArticleDetails($article, opts) {
        var $twixie = $article.find(".article-toggle");
        var $articleMessage = $article.find(".article-message"),
            $articleSections = $article.children(".article-section");
        var collapsedClass = "article-collapsed";
        var expandedClass = "article-expanded";

        var setOpened =  opts && opts.setOpened != null ? opts.setOpened : $article.hasClass(collapsedClass),
            animate = opts ? opts.animate : true,
            beforeExpand = opts && opts.beforeExpand,
            callback = opts && opts.callback;

        var onDone;
        var animateFn;

        if (setOpened) {
            onDone = function () {
                $article.removeClass(collapsedClass).addClass(expandedClass);

                $twixie.text("less");

                expandCommentText($article);

                // We want to remove the styles added by the animation, and fallback to the natural styling
                $articleMessage.children().andSelf().add($articleSections).attr("style", '');

                // this is done because IE can't figure out the width and height of the image until its visible, so we need to trigger it again
                $article.find('.long-message .image-wrap img').trigger('load');

                callback && callback($article, setOpened);
            };

            animateFn = function () {

                var shortHeight = getHeightForArticleMessage($articleMessage, "short-message");

                $articleMessage
                    .css({
                          "height": shortHeight,
                          "max-height": "none",
                          "overflow": "hidden"
                     })
                    .children(".short-message")
                        .hide();

                $articleSections.slideDown();

                beforeExpand && beforeExpand($article);

                $articleMessage.children(".long-message").show();
                var longHeight = getHeightForArticleMessage($articleMessage, "long-message");

                $articleMessage.animate({
                    "height": longHeight
                }, onDone);
            };
        } else {
            onDone = function () {
                $article.addClass(collapsedClass).removeClass(expandedClass);

                $twixie.text("more");
                collapseCommentText($article);

                // We want to remove the styles added by the animation, and fallback to the natural styling
                $articleMessage.children().andSelf().add($articleSections).attr("style", '');

                callback && callback($article, setOpened);
            };

            animateFn = function () {
                var shortHeight = getHeightForArticleMessage($articleMessage, "short-message");
                $articleSections.slideUp();
                $articleMessage.animate({
                    "height": shortHeight
                }, onDone);
            };
        }

        if (animate && animateFn) {
            animateFn();
        } else {
            onDone && onDone();
        }
    };

    function initExpandCollapse($root, callback) {
        $root.delegate(".article-summary", "click", function (event) {
            var $target = $(event.target);
            if ($target.is("a, input, button, select, .input") || $target.parent().is("a, input, button, select, .input")) {
                return;
            }
            var $article = $(this).closest(".article");
            if ($article.hasClass("no-expand")) {
                return;
            }
            var opts = {
                animate: true,
                callback : callback
            };

            toggleArticleDetails($article, opts);
        });
    }

    return {
        initialize : function($root, options) {
            options = options || {};
            initExpandCollapse($root, options.expandCollapseCallback);
        },
        setExpanded : function($article, shouldExpand) {
            toggleArticleDetails($article, {
                setOpened : shouldExpand,
                animate : true
            });
        },
        setAllExpanded : function($articles, shouldExpand) {
            var opts = {
                setOpened: shouldExpand,
                animate: false
            };
            Array.each($articles, function(article) {
                toggleArticleDetails($(article), opts);
            });
        }
    };
})(this, AJS.$);
;
/* END /2static/script/fe/fisheye-article.js */
/* START /2static/script/fe/fisheye-activity.js */
(function($) {

    var article = FE.ARTICLE;

    var triggerPathAbbreviation = function(){};

    FE.setupActivityStream = function () {
        var $streamItems = $("#stream");

        $("#expand-all-activity:not(.disabled)").click(function () {
            var $this = $(this),
                shouldExpand = !$this.hasClass("active"),
                $articles = $streamItems.find(".article");
            
            article.setAllExpanded($articles, shouldExpand);

            if (shouldExpand) {
                Array.each($articles, function(article) {
                    triggerPathAbbreviation($(article), null, false, true);
                });

                $this.addClass("active");
            } else {
                $this.removeClass("active");
            }

            FECRU.PREFS.setPreference("aec", shouldExpand ? "Y" : "N");
        });

        $streamItems.delegate("a.article-create-review-link", "click", function(event) {
            var $target = $(event.target),
                repname = $target.data("repname"),
                csid = $target.data("csid");

            CRU.UTIL.addToReview({ csid: csid, repo: repname });

            //Stops event bubbling and default processing - stops double review creation
            //from both the JS and the href on the a element
            return false;
        });

        article.initialize($streamItems, {
            beforeExpand : function($article) {
                triggerPathAbbreviation($article, null, false, true);
            }
        });

        FECRU.UI.resizeAndCollateImagesToThumbs();

        FE.setupPathAbbreviation($streamItems);
    };

    /**
     * sets up the path abbreviations for the activity stream
     * @param $stream the element that contains the activitystream item. It should contain one or more .file-path elements.
     * @param boundingWidthGetter an optional function, that takes one argument - the path element.
     *          If supplied, it will be used to calculate the width to bound the path.
     */
    FE.setupPathAbbreviation = function($stream, boundingWidthGetter) {
        var $paths = $stream.find('.file-path'), $path;
        if ($paths.length === 0) {
            return;
        }
        var isIE = $.browser.msie;
        if (isIE) {
            //switch to statically truncated version from components
            for( var i = 0; i < $paths.length; i++) {
                $path = $paths.eq(i);
                $path.empty().append($path.siblings('.path-truncated').text());
            }
        } else {
            var triggerPathAbbreviationPerArticle = function($articles, hint) {
                triggerPathAbbreviation($articles, hint, false, true);
            };
            triggerPathAbbreviation = function($this, hint, useAlternateWidthCache, resetBoundsWidth) {
                var $paths = $this.find('.file-path');

                $paths.abbreviatePath({
                    boundingWidthGetter : boundingWidthGetter || function($this) { return $this.closest('li').width()},
                    growingElementGetter : function($this) { return $this.closest('.abbreviate-path-grower'); },
                    directionHint : hint,
                    resetBoundsWidth : resetBoundsWidth,
                    widthCacheKey : useAlternateWidthCache ? 1 : 0
                });
            };

            //initial truncation
            triggerPathAbbreviationPerArticle(AJS.$('.article-expanded'), null);

            //on resize
            var $window = $(window),
                windowWidth = $window.width();
            FECRU.UI.setCompletedResizeTimeout($window, function() {
                var newWindowWidth = $window.width(),
                    shouldRun = windowWidth !== newWindowWidth,
                    hint = windowWidth < newWindowWidth ? 'larger' : 'smaller';
                windowWidth = newWindowWidth;
                if (shouldRun) {
                    triggerPathAbbreviationPerArticle(AJS.$('.article-expanded'), hint);
                }
            }, isIE ? 200 : 50);

            //on collapse/uncollapse sidebar
            AJS.bind('sidebar-expanded', function() {
                triggerPathAbbreviationPerArticle(AJS.$('.article-expanded'), 'smaller');
            }).bind('sidebar-collapsed', function() {
                triggerPathAbbreviationPerArticle(AJS.$('.article-expanded'), 'larger');
            });

            //on mouseenter/mouseleave
            // if we're moving from one path to another, the timing of css updates and js events are off and can cause
            // the paths to be too long (because the width we measure hasn't updated to include the ul.file-views) or
            // too short (because it's still including ul.file-views even though we left).
            // Calling the mouseleave resize on the next paths mouseenter solves this problem.
            var callOnMouseEnter = [];

            $stream.delegate('.stream-files > li', 'mouseenter', function() {
                var $this = $(this),
                    $fileViews = isIE ? $this.find('.file-views') : null;
                if (isIE) { // force display to fix ie bug
                    $fileViews.css('display', 'inline');
                }
                triggerPathAbbreviation($this, 'smaller', true, null);
                if (isIE) {
                    $fileViews.css('display', '');
                }
                while (callOnMouseEnter.length) {
                    triggerPathAbbreviation(callOnMouseEnter.shift(), 'larger', false);
                }
            }).delegate('.stream-files > li', 'mouseleave', function(eventObj) {
                var $this = $(this),
                    $to = $(eventObj.relatedTarget).closest('li');

                if ($to.length) { // lengthen the element you're leaving AFTER shortening
                                  // the one you enter to avoid stretching the bounds.
                    callOnMouseEnter.push($this);
                }
                else {
                    triggerPathAbbreviation($this, 'larger', false);
                }
            });

        }
    };

    AJS.$(document).ready(function(){
        FECRU.UI.resizeAndCollateImagesToThumbs();
    });
})(AJS.$);
;
/* END /2static/script/fe/fisheye-activity.js */
/* START /2static/script/fe/fisheye-changeset.js */
if (!FE.CHANGESET) {
    FE.CHANGESET = {};
}
(function() {
    /////////////////////
    // private functions
    /////////////////////

    /**
     *from http://www.quirksmode.org/js/cookies.html
     */
    function readCookie(name) {
        var nameEQ = name + "=";
        var ca = document.cookie.split(';');
        for (var i = 0; i < ca.length; i++) {
            var c = ca[i];
            while (c.charAt(0) == ' ') {
                c = c.substring(1, c.length);
            }
            if (c.indexOf(nameEQ) == 0) {
                return c.substring(nameEQ.length, c.length);
            }
        }
        return null;
    }

    /*
     * usefull for appending to urls (esp ajax urls) when the content of the
     * url depends on the user's cookie preferences
     */
    function getCookiePrefToken() {
        try {
            var val = readCookie("crucibleprefs1");
            if (val) {
                val = val.replace(/^D=[0-9]+/, "");
                return encodeURIComponent(val);
            }
        } catch(ex) {
            AJS.log("error getting cookie preference:" +
                            ex.message +
                            "\nin:" + ex.fileName +
                            "\nat:" + ex.lineNumber);
        }
        return "unknown";
    }

    function csDisplaySpinner(revId) {
        var divId = csGetCsDivId(revId);
        AJS.$("#" + divId).html("<div class='loading'><span class='loading'>Loading content</span></div>");
        return divId;
    }

    function csGetCsDivId(revId) {
        return 'showfilesDiv' + revId;
    }

    function scrollToSelectedItem() {
        var index = AJS.$("#selectedIndex").val();
        AJS.$("#content-column-panel").scrollTo(AJS.$("#item" + index));
    }

    /////////////////////
    // exported functions
    /////////////////////

    FE.CHANGESET.loadDiff = function (revId, url, onCompleteFunc) {
        var $revDiv = AJS.$("#" + revId);
        if ($revDiv.hasClass("unloaded-diff")) {
            var divId = csDisplaySpinner(revId);
            $revDiv.removeClass("unloaded-diff");
            FECRU.AJAX.ajaxDo(url, {revid: revId, rndtmp: getCookiePrefToken()}, function(resp) {
                if (resp.worked) {
                    // use 'clean' to avoid the regex that jQuery uses to distinguish HTML from ids
                    var replacement = AJS.$.clean([resp.payload], document);
                    AJS.$("#" + divId).html(replacement);
                    scrollToSelectedItem();
                    if (onCompleteFunc) {
                        onCompleteFunc();
                    }
                }
            }, false);
        } else {
            if (onCompleteFunc) {
                onCompleteFunc();
            }
        }
    };

    /**
     * loads a diff asynchronously.
     * @param i
     * @param url
     * @param revIds
     */
    FE.CHANGESET.loadDiffAsync = function (i, url, revIds) {
        var revIdLength = revIds.length;
        var onComp = function(origRequest) {
            // do the chaining.
            FE.CHANGESET.loadDiffAsync(i + 1, url, revIds);
        };
        var MAX_TO_LOAD = 100; //so that if viewing changeset of a branch operation then the page wont overload, set a hard limit.
        if ((i < revIdLength) && (i < MAX_TO_LOAD)) {
            var revId = revIds[i];
            FE.CHANGESET.loadDiff(revId, url, onComp);
        }
    };

    var setupChangesetDiffLoading = function() {
        AJS.$(document).delegate("a.unloaded-diff", "click", function (e) {
            e.preventDefault();
            var $node = AJS.$(this);
            var id = $node.closest(".diffPaneChangset").attr("id");
            var baseClUrl = AJS.$("#baseJSONClUrl").val();
            FE.CHANGESET.loadDiff(id, baseClUrl, null);
        });
    };

    var loadDiffForDt = function($dt) {
        var $revIdDiv = $dt.next().children("div.unloaded-diff");
        // if it is already loaded we won't find that div
        if ($revIdDiv) {
            var baseClUrl = AJS.$("#baseJSONClUrl").val();
            FE.CHANGESET.loadDiff($revIdDiv.attr("id"), baseClUrl, null);
        }
    };

    var isInitialised = false;

    /**
     * Set a property to indicate that the live events etc for the changeset page have been set up
     * @param newValue
     */
    var initialised = function(newValue) {
        if (newValue === undefined) {
            return isInitialised;
        }
        isInitialised = newValue;
        return newValue;
    };

    var reloadChangesetPage = function(page, index) {
        if (page < 1 || page > getNoOfPages()) {
            return;
        }
        var csid = AJS.$("#csid").val();
        var currentPage = getCurrentPageNo();
        if (currentPage == page) {
            AJS.$("#selectedIndex").val(index);
            scrollToSelectedItem();
        } else {
            var $spinner = AJS.$("#file-table-spinner");
            $spinner.show();
            var params = {href: AJS.$("#baseClUrl").val(), csid: csid, pageNum: page, index: index};

            FECRU.AJAX.ajaxDo(FECRU.pageContext + "/json/fe/loadChangesetPage.do", params, function(resp) {
                if (resp.worked) {
                    var replacement = AJS.$.clean([resp.changesetPage], document);
                    AJS.$("#panel-target").replaceWith(replacement);
                    setCurrentPageNo(parseInt(resp.currentPageNo, 10));
                    AJS.$("#selectedIndex").val(resp.selectedIndex);
                    scrollToSelectedItem();
                }
                $spinner.hide();
            }, false);
        }
    };

    function getCurrentPageNo() {
        return parseInt(AJS.$("#currentPageNo").val(), 10);
    }

    function getNoOfPages() {
        return parseInt(AJS.$("#noOfPages").val(), 10);
    }

    function setNavClass(selector, enabled) {
        var links = AJS.$("span.cs-page a").filter(selector);
        if (enabled) {
            links.removeClass("disabled");
        } else {
            links.addClass("disabled");
        }
    }

    function setCurrentPageNo(newPageNo) {
        AJS.$("#currentPageNo").val(newPageNo);
        AJS.$("#currentPageNoDisplay").text(newPageNo);
        setNavClass(".pagination-back, .pagination-first", newPageNo != 1);
        setNavClass(".pagination-next, .pagination-last", newPageNo != getNoOfPages());
    }

    function getAttrValue(node, name) {
        return node.children("input[name='" + name + "']").val();
    }

    FE.CHANGESET.changesetFileLinkFn = function(event) {
        var $node = AJS.$(event.target);
        document.location.hash = getAttrValue($node, "anchor");
        reloadChangesetPage(getAttrValue($node, "page"), getAttrValue($node, "index"));
        return false;
    };

    FE.CHANGESET.changesetPathLinkFn = FE.CHANGESET.changesetFileLinkFn;

    FE.CHANGESET.changesetSubTreeArgs = function($node) {
        return {csid: AJS.$("#csid").val()}
    };

    var setupPaging = function () {
        // We don't put ":not(.disabled)" in the selector otherwise we can't preventDefault for disabled links
        // (as these handlers won't get triggered)
        var handler = function (event, method) {
            event.preventDefault();
            if (!AJS.$(this).is(".disabled")) {
                var args = arguments.length > 2 ? AJS.$.makeArray(arguments).slice(2) : [];
                method && method.apply(this, args);
            }
        };

        var $document = AJS.$(document);

        $document.delegate("span.cs-page a.pagination-back", "click", function (e) {
            handler.call(this, e, reloadChangesetPage, getCurrentPageNo() - 1, 1);
        });
        $document.delegate("span.cs-page a.pagination-next", "click", function (e) {
            handler.call(this, e, reloadChangesetPage, getCurrentPageNo() + 1, 1);
        });
        $document.delegate("span.cs-page a.pagination-first", "click", function (e) {
            handler.call(this, e, reloadChangesetPage, 1, 1);
        });
        $document.delegate("span.cs-page a.pagination-last", "click", function (e) {
            handler.call(this, e, reloadChangesetPage, getNoOfPages(), 1);
        });
    };

    /**
     * Sets up the two detail blocks in the Changeset page and establishes the click responses and stickiness
     */
    function augmentDetails() {
        var $details = AJS.$("#changeset-details"),
            $terse = $details.children("h1.details-terse").children("span"),
            $verbose = $details.children("div.details-verbose"),
            $links = $details.find("a"),
            expandCsHeaderPref = 'xcsh'
        ;

        /**
         * Targets the single line "heading" and adds a click response to reveal the full details.
         * The !e.altKey checks the event properties to ensure that the alt/option key isn't being pressed. This
         * facilitates copy + paste actions
         */
        $terse.click(function(e){
            if (!e.altKey && e.target.tagName.toLowerCase() !== "a") {
               $details.addClass('expand');
               FECRU.PREFS.setPreference(expandCsHeaderPref, 'Y');
            }
        });

        /**
         * Targets the multi line "details" and adds a click response to return to heading only.
         * The !e.altKey checks the event properties to ensure that the alt/option key isn't being pressed. This
         * facilitates copy + paste actions
         * The e.target checks ensure that only the text of the first line of the details will trigger the toggle
         */
        $verbose.click(function(e){
            var target = e.target.tagName.toLowerCase();
            if (!e.altKey && target !== "a" && target !== "div") {
                if (target !== "p" || AJS.$(e.target).is(":first-child")){
                    $details.removeClass('expand');
                    FECRU.PREFS.setPreference(expandCsHeaderPref, 'N');
                }
            }
        });

        /**
         * stops the link from firing if the user is trying to copy the link and also
         * stops the changelog commit message toggling so it won't bump if you're trying
         * to leave the page.
         */
        $links.click(function(e){
            if (e.altKey) {
                e.preventDefault();
                return true;
            }

            e.stopPropagation();
        });
    }

    /**
     * Maintains the UI show/hide principles by checking to see whether this element has any non-whitespace content
     * and displaying if any is found.
     */

    function showCustomDetails() {
        var $custom = AJS.$("td.custom-details"),
            $customDetails = $custom.find("dd"),
            customDetailsHtml = $customDetails.html(),
            details = false;

        if (customDetailsHtml && customDetailsHtml.replace(/\s/g,"").length > 0) {
            $custom.show();
        }
    }

    FE.CHANGESET.init = function() {
        if (!initialised()) {
            augmentDetails();
            showCustomDetails();

            var ajsFecruUi = FECRU.UI;

            ajsFecruUi.changesetToggle(loadDiffForDt)();
            ajsFecruUi.changesetToggleAll();
            ajsFecruUi.registerMetadataExpanders(AJS.$(".changeset-head"));

            setupChangesetDiffLoading();
            setupPaging();

            initialised(true)
        }
    }
})();
;
/* END /2static/script/fe/fisheye-changeset.js */
/* START /2static/script/fe/fisheye-history.js */
(function($) {

    var article = FE.ARTICLE;

    var revIdKey = 'revision-id';

    var revisionIdForRow = function($revision) {
        return $revision.data(revIdKey);
    };

    var rowForRevisionId = function(revisionId) {
        return $("#revision-" + revisionId).closest(".revision");
    };

    // Cache the focused row as it can be too slow to do $("tr.history-focus");
    var $focusedRow,
        focusedRevisionData;
    var blurFocusedRow = function() {
            if ($focusedRow && focusedRevisionData) {
                focusedRevisionData.focussed = false;
                $focusedRow.removeClass("history-focus");
            }
        },
        setRowFocus = function(revisionData) {
            blurFocusedRow();

            //make the switch
            focusedRevisionData = revisionData;
            $focusedRow = rowForRevisionId(revisionData.id);

            //focus the new
            focusedRevisionData.focussed = true;
            if ($focusedRow.length) {
                $focusedRow.addClass("history-focus");
                $(window).scrollTo(rowForRevisionId(focusedRevisionData.id), {
                    duration: 500,
                    axis: 'y',
                    offset: -50
                });
            } else {
                var pageToLoad = getPageForRevisionId(focusedRevisionData.id);
                if (pageToLoad != null) {
                    loadRevisionsPage(pageToLoad, focusedRevisionData.id);
                } else {
                    //TODO: Remove our js error handling from the FECRU."AJAX" namespace...
                    FECRU.AJAX.appendErrorMessage("Couldn't find the revision " + revisionData.revision + ".  Try reloading.");
                    FECRU.AJAX.showErrorBox();
                }
            }
        };

    var revisionDataForId = function (revisionId) {
        var arrayIndex = historyTableData.revisionKeyValueIndexMap[revisionId];
        return historyTableData.revisions[arrayIndex];
    };

    var hasSetupDiffCheckboxes = false;
    /**
     * Binds a live event to validate that only two diff checkboxes can be selected at a time.
     *
     * This method will short circuit if it is called more than once (since we don't want to bind the same live event
     * multiple times).
     */
    var setupDiffCheckboxes = function() {
        if (hasSetupDiffCheckboxes) {
            return;
        }
        hasSetupDiffCheckboxes = true;

        var queue = []; // Queue of checkbox ids in the order that they were checked (oldest -> newest)

        $(document).delegate("#history-revisions input.revision-check", "click", function(event) {
            var $r1 = $("#rev1");
            var $r2 = $("#rev2");

            var id = this.id;
            var value = this.value;

            var revisionId = historyTableData.revisionToRevisionIdMap[value];
            var revisionData = revisionDataForId(revisionId);
            revisionData.checked = !!this.checked;

            var queueLength = queue.length;

            // Nothing is checked, push the id onto the back
            if (queueLength == 0) {
                queue.push(id);
                $r1.val(value);
            }
            // Otherwise, we need to bump the last stack value and shuffle down
            else {
                $r1.val("");
                $r2.val("");

                var removed;
                var queuePosition = $.inArray(id, queue);
                if (queuePosition >= 0) {
                    removed = queue.splice(queuePosition, 1);
                } else {
                    // add the new id to the end of the queue
                    queue.push(id);

                    // if the queue is more than two elements, take off the first n-2 elements
                    if (queue.length > 2) {
                        removed = queue.splice(0, queue.length - 2);
                    }
                }

                if (removed) {
                    for (var i = 0, l = removed.length; i < l; i++) {
                        var rem = removed[i];
                        var removedRevId = rem.replace(/^\D+/, "");
                        revisionData = revisionDataForId(removedRevId);
                        revisionData.checked = false;
                        $("#"+rem).removeAttr("checked");
                    }
                }

                if (queue.length == 2) {
                    $r1.val(revisionDataForId(queue[0].replace(/^\D+/, "")).revision);
                    $r2.val(revisionDataForId(queue[1].replace(/^\D+/, "")).revision);
                }
            }

            var $diffButton = $("#diff-selected-button");
            if ($r1.val() && $r2.val()) {
                $diffButton
                    .removeAttr("disabled")
                    .attr("title", "View the diff between " + $r1.val() + " and " + $r2.val())
                    .closest(".toolbar-item")
                        .removeClass("disabled");
            } else {
                $diffButton
                    .attr("disabled", "disabled")
                    .attr("title", "Select two revisions to view their diff")
                    .closest(".toolbar-item")
                        .addClass("disabled");
            }

            event.stopPropagation();
        });
    };

    var getRevisionFromUrl = function() {
            var hash = document.location.hash;
            if (!hash) {
                return null;
            }
            // remove the '#r' from the anchor
            return hash.substring(2);
        },
        getRevisionIdFromUrl = function() {
            return historyTableData.revisionToRevisionIdMap[getRevisionFromUrl()];
        },
        setupFocusedRevision = function(revisionIdToFocus) {
            if (!revisionIdToFocus) {
                return;
            }

            var revisionData = revisionDataForId(revisionIdToFocus);
            setRowFocus(revisionData);
        };

    var currentPage = null;
    var REVISIONS_PER_PAGE = 30;

    var getTotalPageCount = function () {
        return Math.ceil(historyTableData.revisions.length / REVISIONS_PER_PAGE);
    };

    /**
     * Ensures that the navigation buttons (next/previous revisions) are synchronised -- ie, that they are enabled/disabled
     * when required.
     */
    var syncPaginationButtons = function() {
        var $firstLink = $("a.pagination-first");
        var $prevLink = $("a.pagination-back");
        var $nextLink = $("a.pagination-next");
        var $lastLink = $("a.pagination-last");

        var totalPageCount = getTotalPageCount();
        var show = true;

        if (totalPageCount <= 1) {
            $firstLink.addClass("disabled");
            $prevLink.addClass("disabled");
            $nextLink.addClass("disabled");
            $lastLink.addClass("disabled");
            show = false;
        } else if (currentPage <= 1) {
            $firstLink.removeClass("disabled");
            $prevLink.removeClass("disabled");
            $nextLink.addClass("disabled");
            $lastLink.addClass("disabled");
        } else if (currentPage >= totalPageCount) {
            $firstLink.addClass("disabled");
            $prevLink.addClass("disabled");
            $nextLink.removeClass("disabled");
            $lastLink.removeClass("disabled");
        } else {
            $firstLink.removeClass("disabled");
            $prevLink.removeClass("disabled");
            $nextLink.removeClass("disabled");
            $lastLink.removeClass("disabled");
        }
        var topPagination = $("span.taskbar-pagination");
        var bottomPagination = $("div.toolbar-bottom");
        if (show) {
            topPagination.show();
            bottomPagination.show();
        } else {
            topPagination.hide();
            bottomPagination.hide();
        }

        $(".pagination-text").text(currentPage+"/"+totalPageCount);
    };

    function getPageForRevisionId(revisionId) {
        var revIndex = null;
        Array.first(historyTableData.revisions, function(revision, index) {
            if (revision.id === revisionId) {
                revIndex = index;
                return true;
            }
            return false;
        });
        if (revIndex === null) {
            return null;
        }

        return Math.floor(revIndex / REVISIONS_PER_PAGE) + 1;
    }

    /**
     * Loads revisions into the history table for display. This method will lazily load extra information from the server
     * if it isn't available locally (such as rendered commit messages).
     * @param pageNumber page to render
     */
    var loadRevisionsPage = function(pageNumber, revisionIdToFocus) {
        if (currentPage === pageNumber) {
            throw new Error("Trying to load current page.");
        }

        pageNumber = pageNumber || 1;
        currentPage = pageNumber;

        var totalPageCount = getTotalPageCount();
        if (pageNumber > totalPageCount) {
            pageNumber = totalPageCount;
        }

        blurFocusedRow();

        var allRevisions = historyTableData.revisions;
        var $historyTable = $("#history-revisions");
        $historyTable
                .removeClass("data-loaded")
                .addClass("data-loading")
                .empty();

        var startPoint = (pageNumber-1) * REVISIONS_PER_PAGE;
        if (startPoint < 0) {
            startPoint = 0;
        }
        var endPoint = startPoint + REVISIONS_PER_PAGE;

        var revIdsToLoad = [];

        for (var c = 0, i = startPoint, revCount = allRevisions.length;
             i < endPoint && i < revCount;
             i++, c++) {
            var rev = allRevisions[i];

            if (!rev.loaded) {
                revIdsToLoad.push(rev.id);
            }
        }

        syncPaginationButtons();

        loadRevisionData(revIdsToLoad, function () {
            var $revisionElementsToInsert = $([]);

            for (var c = 0, i = startPoint, revCount = allRevisions.length;
            i < endPoint && i < revCount;
            i++, c++) {
               var rev = allRevisions[i];

               var $revElem = $(rev.htmlContent).data(revIdKey, rev.id);
               article.setExpanded($revElem, !rev.collapsed);

               if (rev.focussed) {
                   $revElem.addClass("history-focus");
               }

               $revisionElementsToInsert.push($revElem.get(0));
            }
            $historyTable.append($revisionElementsToInsert);

            $historyTable
            .addClass("data-loaded")
            .removeClass("data-loading");


            if (revisionIdToFocus) {
                setupFocusedRevision(revisionIdToFocus);
            }
        });
    };

    /**
     * Sends a list of revisions to the server for extra data which is required for displaying complete data for each
     * revision to the user. Upon completion of the ajax call, retrieved data is stored locally for later reuse.
     * @param revisions array of revisions to complete
     * @param onDone callback function
     */
    var loadRevisionData = function (revisions, onDone) {
        if (!revisions || revisions.length == 0) {
            onDone && onDone();
            return;
        }
        var done = function (resp) {
            var revs = resp.revisions;
            if (!revs) {
                return;
            }
            for (var i = 0, len = revs.length; i < len; i++) {
                var rev = revs[i];

                // Update the data structure
                var revisionData = revisionDataForId(rev.id);

                revisionData.htmlContent = rev.htmlContent;
                revisionData.loaded = !!revisionData.htmlContent;
            }
            onDone && onDone();
        };

        var params = {
            content: true,
            rev: revisions
        };

        var pathName = window.location.pathname;
        pathName = pathName.substring(FECRU.pageContext.length);
        var url = FECRU.pageContext + "/json" + pathName;

        FECRU.AJAX.ajaxDo(url, params, done);
    };

    /**
     * Create a map between each revision id and it's index in the {{historyTableData.revisions}} array. This method
     * should be called whenever the order of the revisions data is changed, such as on sorting.
     *
     * This data structure is used so that we can efficiently look up revisionData objects and update them as we lazily
     * retrieve expensive data from the server.
     */
    var createRevisionKeyIndexMap = function() {
        var allRevisions = historyTableData.revisions;
        var revisionKeyValueIndexMap = {};
        var revisionToRevisionIdMap = {};
        if (allRevisions) {
            for (var i = 0, len = allRevisions.length; i < len; i++) {
                var rev = allRevisions[i];
                revisionKeyValueIndexMap[rev.id] = i;
                revisionToRevisionIdMap[rev.revision] = rev.id;
            }
        }
        historyTableData.revisionKeyValueIndexMap = revisionKeyValueIndexMap;
        historyTableData.revisionToRevisionIdMap = revisionToRevisionIdMap;
    };

    var expandCollapseCallback = function ($article, isExpanded) {
        var revData = revisionDataForId(revisionIdForRow($article));
        revData.collapsed = !isExpanded;
    };

    FE.setupFileHistoryPage = function () {
        createRevisionKeyIndexMap();

        setupDiffCheckboxes();

        var pageNumberToLoad = 1; // We start at 1
        var revisionIdToLoad = getRevisionIdFromUrl();
        if (revisionIdToLoad) {
            var arrayIndexToLoad = historyTableData.revisionKeyValueIndexMap[revisionIdToLoad];
            pageNumberToLoad = Math.floor(arrayIndexToLoad / REVISIONS_PER_PAGE) + 1; // pageNumber starts at 1
        }

        loadRevisionsPage(pageNumberToLoad, revisionIdToLoad);

        $("a.pagination-back").click(function () {
            if ($(this).hasClass("disabled")) {
                return false;
            }
            loadRevisionsPage(currentPage+1); // next page
            return false;
        });
        $("a.pagination-next").click(function () {
            if ($(this).hasClass("disabled")) {
                return false;
            }
            loadRevisionsPage(currentPage-1); // previous page
            return false;
        });
        $("a.pagination-last").click(function () {
            if ($(this).hasClass("disabled")) {
                return false;
            }
            loadRevisionsPage(1); // last page
            return false;
        });
        $("a.pagination-first").click(function () {
            if ($(this).hasClass("disabled")) {
                return false;
            }
            var totalPageCount = getTotalPageCount();
            loadRevisionsPage(totalPageCount); // first page
            return false;
        });

        var $historyRevisions = $("#history-revisions");

        article.initialize($historyRevisions, {
            expandCollapseCallback : expandCollapseCallback
        });

        $("#expand-all-revisions").click(function () {
            var allRevisions = historyTableData.revisions;
            if (!allRevisions || allRevisions.length === 0) {
                return;
            }

            var $this = $(this).parent(),
                shouldExpand = !$this.hasClass("active"),
                $articles = $historyRevisions.children(".article");

            article.setAllExpanded($articles, shouldExpand);

            Array.each(allRevisions, function(rev) {
                rev.collapsed = !shouldExpand;
            });

            if (shouldExpand) {
                $this.addClass("active");
            } else {
                $this.removeClass("active");
            }
        });

        FECRU.UI.registerMetadataExpanders($historyRevisions);

        // handle IE7 z-index bug for positioned elements
        if ($.browser.msie && $.browser.version < 8) {
            $(".aui-dd-link").live("click", function() {
                var $this = $(this);

                // restore the default z-index for all the revisions
                $(".revision.ie-zindex-fixer").removeClass("ie-zindex-fixer");

                // make the parent revision above the subsequent one(s) and the dropdown above the subsequent "more" button
                var $revision = $this.closest(".revision").addClass("ie-zindex-fixer");

                // bind a IE-specific event handler to know when the dropdown becomes hidden
                // (unbind first to prevent duplicate bindings if the user clicks several times)
                var dropdown = $(this).siblings(".aui-dropdown");
                dropdown.unbind("propertychange").bind("propertychange", function() { // does not work with .live()
                    var $dropdown = $(this);
                    if ($dropdown.is(":not(:visible)")) {
                        $revision.removeClass("ie-zindex-fixer");
                        $dropdown.unbind("propertychange");
                    }
                });
            });
        }

        // setup local-link links
        $(document).delegate("a.local-link", 'click', function(e) {
            var href = $(this).attr("href");
            var revision = href.substring(href.lastIndexOf("#") + 2);
            var revisionId = historyTableData.revisionToRevisionIdMap[revision];
            var revisionData = revisionDataForId(revisionId);
            if (revisionId) {
                setRowFocus(revisionData);
            }
            e.preventDefault();
        });
    };

    // Following methods not used for the file history - move them elsewhere
    FE.fileHistoryPathLinkFn = function(event) {
        event.stopPropagation();
        return true;
    };
})(AJS.$);
;
/* END /2static/script/fe/fisheye-history.js */
/* START /2static/script/lib/json2.min.js */
/* json2.js
 * 2008-01-17
 * Public Domain
 * No warranty expressed or implied. Use at your own risk.
 * See http://www.JSON.org/js.html
*/
if(!this.JSON){JSON=function(){function f(n){return n<10?'0'+n:n;}
Date.prototype.toJSON=function(){return this.getUTCFullYear()+'-'+
f(this.getUTCMonth()+1)+'-'+
f(this.getUTCDate())+'T'+
f(this.getUTCHours())+':'+
f(this.getUTCMinutes())+':'+
f(this.getUTCSeconds())+'Z';};var m={'\b':'\\b','\t':'\\t','\n':'\\n','\f':'\\f','\r':'\\r','"':'\\"','\\':'\\\\'};function stringify(value,whitelist){var a,i,k,l,r=/["\\\x00-\x1f\x7f-\x9f]/g,v;switch(typeof value){case'string':return r.test(value)?'"'+value.replace(r,function(a){var c=m[a];if(c){return c;}
c=a.charCodeAt();return'\\u00'+Math.floor(c/16).toString(16)+
(c%16).toString(16);})+'"':'"'+value+'"';case'number':return isFinite(value)?String(value):'null';case'boolean':case'null':return String(value);case'object':if(!value){return'null';}
if(typeof value.toJSON==='function'){return stringify(value.toJSON());}
a=[];if(typeof value.length==='number'&&!(value.propertyIsEnumerable('length'))){l=value.length;for(i=0;i<l;i+=1){a.push(stringify(value[i],whitelist)||'null');}
return'['+a.join(',')+']';}
if(whitelist){l=whitelist.length;for(i=0;i<l;i+=1){k=whitelist[i];if(typeof k==='string'){v=stringify(value[k],whitelist);if(v){a.push(stringify(k)+':'+v);}}}}else{for(k in value){if(typeof k==='string'){v=stringify(value[k],whitelist);if(v){a.push(stringify(k)+':'+v);}}}}
return'{'+a.join(',')+'}';}}
return{stringify:stringify,parse:function(text,filter){var j;function walk(k,v){var i,n;if(v&&typeof v==='object'){for(i in v){if(Object.prototype.hasOwnProperty.apply(v,[i])){n=walk(i,v[i]);if(n!==undefined){v[i]=n;}}}}
return filter(k,v);}
if(/^[\],:{}\s]*$/.test(text.replace(/\\./g,'@').replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,']').replace(/(?:^|:|,)(?:\s*\[)+/g,''))){j=eval('('+text+')');return typeof filter==='function'?walk('',j):j;}
throw new SyntaxError('parseJSON');}};}();};
/* END /2static/script/lib/json2.min.js */
/* START /2static/script/cru/util.js */
/**
 * Crucible utility functions that can be used from any crucible page.
 */

window.CRU = window.CRU || {};
CRU.UTIL = {};

(function ($) {
    var cruUtil = CRU.UTIL;
    /**
     * Crucible base url (without trailing '/').
     *
     * @param permaId optional review id
     */
    cruUtil.urlBase = function (permaId) {
        if (permaId) {
            return FECRU.pageContext + '/cru/' + permaId;
        } else {
            return FECRU.pageContext + '/cru';
        }
    };

    /**
     * Crucible JSON base url (without trailing '/').
     *
     * @param permaId optional review id
     */
    cruUtil.jsonUrlBase = function (permaId) {
        if (permaId) {
            return FECRU.pageContext + '/json/cru/' + permaId;
        } else {
            return FECRU.pageContext + '/json/cru';
        }
    };
    
    cruUtil.isReviewPage = function () {
        return typeof review !== 'undefined';
    };


    cruUtil.startAjaxDialogSpin = function () {
        $('body').addClass('ajax-dialog');
        AJS.dim();
    };

    cruUtil.stopAjaxDialogSpin = function () {
        AJS.undim();
        $('body').removeClass('ajax-dialog');
    };

    cruUtil.isAjaxDialogSpinning = function () {
        return cruUtil.isDimmed() && $('body').hasClass('ajax-dialog');
    };

    cruUtil.ajaxDialog = function (url, params) {
        cruUtil.startAjaxDialogSpin();
        FECRU.AJAX.ajaxDo(url, params || {}, function (resp) {
            if (resp.worked) {
                if (resp.showDialog) {
                    cruUtil.stopAjaxDialogSpin();
                    try {
                        FECRU.DIALOG.getAjaxDialogContainer().html(resp.payload);
                        FECRU.DIALOG.triggerAjaxDialogLoaded();
                    } catch (e) {
                        alert(e);
                    }
                } else if (resp.redirect) {
                    // no need to undim
                    window.location = resp.payload;
                }
            }
            // !resp.worked handled by ajaxDo
        });
        return false;
    };

    cruUtil.stateTransition = function (transition, permaId, params) {
        var util = cruUtil;
        var url = util.jsonUrlBase(permaId) + '/changeStateAjax';
        var unsaved = CRU.UNSAVED;

        // Make sure there aren't any unsaved inputs on the page, and if there are, then provide a warning
        // and give the user the ability to cancel and save their inputs.
        if (unsaved) {
            if (!unsaved.confirmUnsubmittedInputs()) {
                return;
            }
            unsaved.clearWatchForUnsavedChanges();
        }

        params = params || {};

        $.extend(params, {
            command: transition
        });

        if (util.isReviewPage() && $.inArray(transition, ['action:completeReview', 'action:summarizeReview']) >= 0) {
            // If completing or summarizing we need to warn if the review has been updated.

            util.startAjaxDialogSpin();
            CRU.REVIEW.UTIL.reviewUpdatedAjax({
                done: function () {
                    var reviewUpdated = $('body').hasClass('review-updated');
                    if (reviewUpdated) {
                        CRU.REVIEW.UTIL.warnAboutReviewUpdates({ reshowWarning: true });
                    }
                    util.stopAjaxDialogSpin();
                    return util.ajaxDialog(url, $.extend(params, { reviewUpdated: reviewUpdated }));
                }
            });
            return false;

        } else {
            return util.ajaxDialog(url, params);
        }
    };

    cruUtil.editDetailsFormChange = false;
    cruUtil.checkEditForm = function (done) {
        if (cruUtil.editDetailsFormChange) {
            CRU.REVIEW.UTIL.postEditDetailsForm(done);
        } else {
            if (done) {
                done();
            }
        }
        return false;//do a link action if called from <a>
    };

    cruUtil.command = function (cmd, pid, button) {
        var perma = pid || permaId;
        if (button) {
            button.disabled = true;
        }
        var donext = function() {
            var url = cruUtil.urlBase(perma);
            window.location = url + '/' + cmd;
        };
        //check and post the editDetailsForm if it has changed
        cruUtil.checkEditForm(donext);
    };


    cruUtil.createBlankReview = function (params) {
        var url = cruUtil.jsonUrlBase() + '/createReviewDialog';
        return cruUtil.ajaxDialog(url, params);
    };

    cruUtil.addToReview = function (params) {
        var url = cruUtil.jsonUrlBase() + '/createDialog';
        return cruUtil.ajaxDialog(url, params);
    };

    cruUtil.isAnyDialogShowing = function () {
        return $('body').children('div.dialog:visible').length > 0;
    };

    cruUtil.isDimmed = function () {
        return !!AJS.dim.dim;
    };

    cruUtil.makeCssRule = function (selector, ruleBody) {
        var styleSheet = document.styleSheets[0];
        var index = 0;
        if (styleSheet.insertRule) {
            styleSheet.insertRule(selector + '{' + ruleBody + '}', index);
            return styleSheet.cssRules[index].style;
        } else {
            // Internet Explorer's version.
            styleSheet.addRule(selector, ruleBody, index);
            return styleSheet.rules[index].style;
        }
    };

    cruUtil.createIdeSrc = function(linkUrl, frxId) {
        var src = linkUrl + '&id=' + Math.floor(Math.random()*1000);
        if (frxId) {
            src += '&line=' + CRU.REVIEW.UTIL.getTopVisibleLineNumber(frxId);
        }
        return src;
    };

    cruUtil.loadJiraIssueLink = function (issueKey, $target, params) {
        if (!issueKey) {
            throw "JIRA issue key required";
        }
        if (!$target) {
            throw "Target container to insert issue details into required";
        }
        var defaults = {
            showTitle: true,
            maxTitleLength: 75,
            key: issueKey
        };

        var data = $.extend({}, defaults, params);

        var getIssueDetails = function () {
            AJS.$.ajax({
                url: FECRU.pageContext + '/json/action/issue-inline.do',
                data: data,
                type: "GET",
                dataType: "json",
                success: function(resp) {
                    $target.html(resp.html);

                    $target.find(".ual-authenticate").bind("click", function () {
                        FECRU.OAUTH.getEventProducer($(this).attr("href"))
                            .authorized(function() {
                                FECRU.HOVER.invalidateCache(FECRU.HOVER.CACHE_FOREVER, issueKey);
                                getIssueDetails();
                            });
                    });
                },
                error: function (jqXHR, textStatus, errorThrown) {
                    var error = "Error retriving issue " + issueKey;
                    if (errorThrown) {
                        error += ": " + errorThrown;
                    }
                    $target.text(error);
                }
            });
        };

        getIssueDetails();
    };

})(AJS.$);
;
/* END /2static/script/cru/util.js */
/* START /2static/script/cru/dialog/dialog-event.js */
(function () {

    var $container = FECRU.DIALOG.getAjaxDialogContainer();

    var dialogHandler = function (callbacks) {
        return function (event) {
            var dialog = $container.data('dialog');
            var permaId = $container.data('permaId');

            if (CRU.UTIL.isReviewPage() && callbacks.review) {
                return callbacks.review(dialog, permaId, event);
            } else if (!CRU.UTIL.isReviewPage() && callbacks.external) {
                return callbacks.external(dialog, permaId, event);
            }
        };
    };

    var viewFiltersHandler = function (filters) {
        return dialogHandler({
            review: function (dialog) {
                if (AJS.$('body').hasClass('review-updated')) {
                    window.location.hash = 'f-' + filters.join(',');
                    window.location.reload(true);
                } else {
                    CRU.REVIEW.UTIL.filterAndExpandFrxs(filters);
                    dialog.remove();
                }
            },
            external: function (dialog, permaId) {
                window.location = CRU.UTIL.urlBase(permaId) + '#f-' + filters.join(',');
            }
        });
    };

    var batchProcessDraftComments = function (action, permaId, onComplete) {
        var url = CRU.UTIL.jsonUrlBase(permaId) + '/draftCommentsAjax';
        var params = {
            action: action
        };
        var done = function (resp) {
            if (onComplete) {
                onComplete(resp);
            }
        };
        FECRU.AJAX.ajaxDo(url, params, done, true);
    };

    var draftCommentsHandler = function (action) {
        return dialogHandler({
            review:  function (dialog) {
                AJS.$.each(review.draftComments(), function (i, comment) {
                    commentator[action + 'Comment'](comment.id(), review.id());
                });
                if (action == 'delete') {
                    AJS.$("#dialog-drafts-message").html("Your draft comments have been deleted.");
                } else if (action == 'publish') {
                    AJS.$("#dialog-drafts-message").html("Your draft comments have been posted.");
                }
                AJS.$('#dialog-drafts-links').hide();
            },
            external: function (dialog, permaId) {
                batchProcessDraftComments(action, permaId, function (resp) {
                    if (resp.worked) {
                        AJS.$('#dialog-drafts-links').hide();
                    } else {
                        // TODO Show error message within dialog panel.
                    }
                });
            }
        });
    };

    var resolveUnresolvedJiras = function () {
        var doIt = function(dialog, permaId) {
            AJS.$('#dialog-unresolved-jiras-controls').hide();
            var url = CRU.UTIL.jsonUrlBase(permaId) + '/resolveAllSubtasksAjax';
            var params = {};
            AJS.$('#dialog-unresolved-jiras-spinner').show();
            var done = function (resp) {
                AJS.$('#dialog-unresolved-jiras-spinner').hide();
                if (resp.worked) {
                    AJS.$('#dialog-unresolved-jiras-title').html("There are no unresolved subtasks.").show();
                } else {
                    AJS.$('#dialog-unresolved-jiras-results').addClass("jira-error").html(resp.errorMsg).show();
                }
            };
            FECRU.AJAX.ajaxDo(url, params, done, true);
        };
        return dialogHandler({review: doIt, external: doIt});
    };

    AJS.$(document).ready(function () {
        var $document = AJS.$(document);
        $document.delegate('#dialog-view-drafts', 'click', viewFiltersHandler(['draftcomments']));
        $document.delegate('#dialog-view-unread-comments', 'click', viewFiltersHandler(['unreadcomments']));
        $document.delegate('#dialog-view-incomplete-frxs', 'click', viewFiltersHandler(['incomplete']));
        $document.delegate('#dialog-view-unresolved-jiras', 'click', viewFiltersHandler(['unresolvedsubtasks']));

        $document.delegate('#dialog-delete-drafts', 'click', draftCommentsHandler('delete'));
        $document.delegate('#dialog-post-drafts', 'click', draftCommentsHandler('publish'));
        $document.delegate('#dialog-resolve-unresolved-jiras', 'click', resolveUnresolvedJiras());
    });

})();
;
/* END /2static/script/cru/dialog/dialog-event.js */
/* START /2static/script/cru/review/email-comments.js */
window.CRU = window.CRU || {};
if (!CRU.REVIEW) {
    CRU.REVIEW = {};
}
if (!CRU.REVIEW.EMAILCOMMENTS) {
    CRU.REVIEW.EMAILCOMMENTS = {};
}

(function() {
    var dialog;
    var self = CRU.REVIEW.EMAILCOMMENTS;

    var nextButton = function () {};
    var previousButton = function () {};

    self.showPage = function (pageId) {

        AJS.$(".email-wizard", "#emailWizardDialog").each(function() {
            if (AJS.$(this).is('#' + pageId)) {
                AJS.$(this).show();
            } else {
                AJS.$(this).hide();
            }
        });
        AJS.$("#emailWizardDialog").find(".previousButton, .nextButton, .cancelButton").each(function() {
            AJS.$(this)
                .attr('disabled', false)
                .show();
        });

        if (pageId === 'recipients') {
            AJS.$(".nextButton", "#emailWizardDialog").html("Next");
            nextButton = function () {
                self.showPage('message');
            };
            AJS.$(".previousButton", "#emailWizardDialog")
                    .hide();
            previousButton = function () {};

        } else if (pageId === 'message') {
            AJS.$(".nextButton", "#emailWizardDialog").html("Send");
            nextButton = function () {
                var $form = AJS.$("#emailWizardDialog form#emailCommentsForm");
                var url = $form.attr('action');
                AJS.$(".nextButton", "#emailWizardDialog").attr("disabled", true);
                FECRU.AJAX.ajaxUpdate(url, $form.serialize(), "dialogPanel");
            };
            previousButton = function () {
                self.showPage('recipients');
            };

        } else if (pageId === 'sent') {
            AJS.$(".nextButton", "#emailWizardDialog").html("Close");
            nextButton = function () {
                dialog.remove();
            };
            previousButton = function () {
                self.showPage('message');
            };
        }
    };

    self.start = function (permaId) {

        dialog = FECRU.DIALOG.ajaxDialog(900, 600, {}, "emailWizardDialog")
            .addHeader("Email Review Comments")
            .addPanel("Recipients", "<div id='dialogPanel'>Loading...</div>")
            .addButton("Cancel", function(dialog) {
                dialog.remove();
            }, "cancelButton")
            .addButton("Previous", function() {
                previousButton();
            }, "previousButton")
            .addButton("Next", function() {
                nextButton();
            }, "nextButton")
            .show();

        var url = CRU.UTIL.jsonUrlBase(permaId) + "/allcommentsemail";

        var ajax = FECRU.AJAX;
        ajax.startSpin("dialogPanel", "email-spinner");
        ajax.ajaxUpdate(url, {}, "dialogPanel", function () {
            ajax.stopSpin(AJS.$('#dialogPanel'));
            AJS.$("form#emailCommentsForm").find("#to").focus();
        });
    };
} )();
;
/* END /2static/script/cru/review/email-comments.js */
/* START /2static/script/cru/review/review-history.js */
window.CRU = window.CRU || {};
if (!CRU.REVIEW) {
    CRU.REVIEW = {};
}
if (!CRU.REVIEW.HISTORY) {
    CRU.REVIEW.HISTORY = {};
}

CRU.REVIEW.HISTORY.showPage = function (permaId) {
    AJS.dropDown.current.hide();

    if (!permaId) {
        AJS.log("I don't know which review to give you history for.");
        return;
    }

    var dialog = FECRU.DIALOG.create(1200, 700, "cru-review-history-dialog");

    var iframeStyle = "style='overflow:hidden;width:100%;height:" + (dialog.height - 43 - 44 - 23) + "px'";
    var cs = "<iframe frameborder='0' id='reviewHistoryIframe' name='reviewHistoryIframe' scrolling='no' src='" + CRU.UTIL.urlBase(permaId) + "/reviewHistoryWrapper" + "' " + iframeStyle + "></iframe>";

    var header = "History of Review " + permaId;
    dialog.addHeader(header)
            .addPanel("History", cs);

    dialog.addButton("Done", function(dialog) {
        dialog.hide();
    }).show();

    AJS.$("#cru-review-history-dialog").data("dialog", dialog);//stores the object so we can access it from its contents
};

CRU.REVIEW.HISTORY.setupLinks = function() {
    var $document = AJS.$(document);
    $document.delegate("a.action-link", "click", function () {
        parent.top.AJS.$("#cru-review-history-dialog").data("dialog").hide();
    });
    $document.delegate("a.comment-link", "click", function () {
        var comment_nav = parent.top.CRU.COMMENT.NAV;
        var comment_id = AJS.$(this).attr('hash').replace('#c', '');

        var scrollToMap = comment_nav.navigateFindComment({ commentId: comment_id });
        comment_nav.navigateDirectlyToElement({ commentId: comment_id }, scrollToMap);
    });
    $document.delegate("a.frx-link", "click", function () {
        var frx_id = AJS.$(this).attr('hash').replace('#CFR-','');

        parent.top.CRU.FRX.NAV.gotoFrx({ frxId: frx_id , destination: '' });
    });

};
;
/* END /2static/script/cru/review/review-history.js */

