Source: singletons/lib.js

/** @namespace */
Syntree = {}; // Single global object, append any other 'globals' to this

/**
 * Data on what properties can be configured onto given object types.
 *
 * @property classname {object} - the data for each class
 * @property classname.accept_unmapped_config {boolean} - whether or not accept configuration properties that are not represented in the config map
 * @property classname.map {object} - an object representing the possible configuration properties, their required types, and the default values to provide if the type check is not passed
 * @type {object}
 * @see Syntree.Lib.config
 */
Syntree.config_maps = {};

Syntree.initialize = function(initial_matrix) {
    /**
     * The snap object tied to our svg workspace.
     *
     * @memberof Syntree
     */
    Syntree.snap = Snap('#workspace');
    Syntree.Workspace.initialize(initial_matrix);
}

/** @class
 * @classdesc What you'd expect -- various utility and cross-class functions.
 */
Syntree.Lib = {
    /**
     * Add properties to a given object, using that object's config_map property to check types and apply defaults.
     *
     * @param {object} matrix - An object of properties to be appended to the target
     * @param {object} target - The object to be 'configured'
     */
    config: function(matrix, target) {
        var targetType = Syntree.Lib.typeOf(target);
        var map = Syntree.config_maps[targetType].map;
        var accept_unmapped_config = Syntree.config_maps[targetType].accept_unmapped_config;

        for (property_name in map) {
            var require = map[property_name].require;
            var default_value = map[property_name].default_value;

            if (!this.checkType(matrix[property_name], require)) {
                if (this.checkType(default_value, 'undefined')) {
                    // If default_value is undefined, we assume that the property is required.
                    throw new Error('You must provide a value for "' + property_name + '"');
                } else {
                    // If default_value is the special string '#undefined', we assume that it should be allowed to remain undefined.
                    // Otherwise, we use the default_value.
                    if (default_value !== '#undefined') {
                        target[property_name] = default_value;
                    }
                }
            } else {
                target[property_name] = matrix[property_name];
            }
        }

        if (typeof accept_unmapped_config === 'boolean' && accept_unmapped_config) {
            for (property_name in matrix) {
                if (typeof map[property_name] === 'undefined') {
                    target[property_name] = matrix[property_name];
                }
            }
        }
    },

    /**
     * Just a function that allows us to focus an element without auto-scrolling to it.
     * Useful if the app is embedded in a larger page.
     *
     * @param {jQuery_Object} elem - A page element to scroll to
     */
    focusNoScroll: function(elem) {
      var x = window.scrollX;
      var y = window.scrollY;
      elem.focus();
      window.scrollTo(x, y);
    },

    /**
     * Keeps track of ids that have been generated.
     * @see Syntree.Lib.genId
     */
    allIds: [],

    /**
     * The upper bound of random number generation for ids.
     * Increases if we get too close.
     *
     * @see Syntree.Lib.genId
     */
    idN: 1000,

    /**
     * Generates a unique id (unique within this session).
     *
     * @see Syntree.Lib.allIds
     * @see Syntree.Lib.idN
     * @returns {number} a session-unique id
     */
    genId: function() {
        if (this.allIds.length === this.idN / 2) {
            this.idN += 1000;
        }
        while (true) {
            var x = Math.floor(Math.random() * this.idN);
            if (this.allIds.indexOf(x) === -1) {
                this.allIds.push(x)
                return x;
            }
        }
    },

    /**
     * Get the type of anything, taking into account all kinds of JS type weirdness.
     * Returns undefined for NaN and null. Returns specific object type if available, 'object' otherwise.
     *
     * @param {} a - any value
     * @returns {string} the type of the passed value
     */
    typeOf: function(a) {
        // Modified from http://stackoverflow.com/questions/13926213/checking-the-types-of-function-arguments-in-javascript
        var type = ({}).toString.call(a).match(/\s(\w+)/)[1].toLowerCase();
        if (type === 'object') {
            var t = a.toString().match(/\s(\w+)/)[1].toLowerCase();
            // If object's toString returns a valid custom type string, return it
            if (a.toString().match(/\[\w+\s\w+\]/)) {
                return t;
            // Otherwise, return the default type string
            } else {
                return type;
            }
        } else if (type === 'number' && a !== a) {
            return 'NaN';
        } else {
            return type;
        }
    },

    /**
     * Check a value against any given type(s).
     *
     * @param {} a - any value
     * @param {string|string[]} required_type - a string representing the required type, or an array of such strings
     * @returns {boolean} whether the passed value matched the required type(s)
     */
    checkType: function(a, require) {
        if (this.typeOf(require) === 'string') {
            return this.typeOf(a) === require;
        } else if (this.typeOf(require) === 'array') {
            var i = 0;
            while (i < require.length) {
                if (this.typeOf(a) === require[i]) {
                    return true;
                }
                i++;
            }
            return false;
        } else if (this.typeOf(require) === 'function') {
            // Call require function in context of passed object, because it is probably a method
            var r = require.call(a);
            if (this.typeOf(r) === 'boolean') {
                return r;
            } else {
                throw new TypeError('The require function must return true or false');
            }
        } else {
            throw new TypeError('Please pass checkType a type string, array of type strings, or a function that returns true/false (for the second argument)');
        }
    },

    /**
     * Ideal for checking argument types. Checks the passed value against the required type,
     * and returns the default value instead if the check doesn't pass.
     * A default value of '#undefined' will permit the type check to fail, and return nothing.
     * Otherwise (if default_value is actually undefined), will throw an error on type check failure.
     *
     * @param {} passed - any value
     * @param {string|string[]|function} require - a string representing the required type, an array of such strings, or a function returning true/false
     * @param {} default_value - any value, to be returned if the type check fails
     */
    checkArg: function(a, require, default_value) {
        if (this.checkType(require, ['string', 'array', 'function'])) {
            if (this.checkType(a, require)) {
                return a;
            } else {
                if (!this.checkType(default_value, 'undefined')) {
                    if (default_value === '#undefined') {
                        return;
                    } else {
                        return default_value;
                    }
                } else {
                    throw new TypeError('Argument is required to be type ' + String(require).replace(',', ' or ') + ', was type ' + this.typeOf(a));
                }
            }
        } else {
            throw new TypeError('Please pass checkArg a type string, array of type strings, or a function that returns true/false (for the second argument)');
        }
    },

    /**
     * Get the distance between two points.
     *
     * @param {number|object} x1_or_obj - either the x coordinate of point 1, or an object representing all four coordinates
     * @param {number} [y1] - the y coordinate of point 1
     * @param {number} [x2] - the x coordinate of point 2
     * @param {number} [y2] - the y coordinate of point 2
     * @returns {number} the distance between the two points
     */
    distance: function(x1_or_obj,y1,x2,y2) {
        if (this.checkType(x1_or_obj, 'object')) {
            x1 = Syntree.Lib.checkArg(x1_or_obj.x1, 'number');
            y1 = Syntree.Lib.checkArg(x1_or_obj.y1, 'number');
            x2 = Syntree.Lib.checkArg(x1_or_obj.x2, 'number');
            y2 = Syntree.Lib.checkArg(x1_or_obj.y2, 'number');
        } else {
            x1 = Syntree.Lib.checkArg(x1_or_obj, 'number');
            y1 = Syntree.Lib.checkArg(y1, 'number');
            x2 = Syntree.Lib.checkArg(x2, 'number');
            y2 = Syntree.Lib.checkArg(y2, 'number');
        }

        return Math.sqrt(Math.pow((x2 - x1),2)+Math.pow((y2 - y1),2));
    },

    /**
     * Capitalize the first letter of a string.
     * Often used for converting types into corresponding constructor function identifiers.
     *
     * @param {string} string - any string
     * @returns {string} the passed string, with the first letter capitalized
     */
    capitalize: function(string) {
        return string[0].toUpperCase() + string.slice(1, string.length);
    },

    /**
     * Get the mid point of a line spanning two points
     *
     * @param {number|object} x1_or_obj - either the x coordinate of point 1, or an object representing all four coordinates
     * @param {number} [y1] - the y coordinate of point 1
     * @param {number} [x2] - the x coordinate of point 2
     * @param {number} [y2] - the y coordinate of point 2
     * @returns {object} - the x/y coordinates of the mid point
     */
    getMidPoint: function(x1_or_obj,y1,x2,y2) {
        if (this.checkType(x1_or_obj, 'object')) {
            x1 = Syntree.Lib.checkArg(x1_or_obj.x1, 'number');
            y1 = Syntree.Lib.checkArg(x1_or_obj.y1, 'number');
            x2 = Syntree.Lib.checkArg(x1_or_obj.x2, 'number');
            y2 = Syntree.Lib.checkArg(x1_or_obj.y2, 'number');
        } else {
            x1 = Syntree.Lib.checkArg(x1_or_obj, 'number');
            y1 = Syntree.Lib.checkArg(y1, 'number');
            x2 = Syntree.Lib.checkArg(x2, 'number');
            y2 = Syntree.Lib.checkArg(y2, 'number');
        }

        return {
            x: (x1 + x2) / 2,
            y: (y1 + y2) / 2,
        }
    },

    /**
     * Take screen/visual coordinates and convert them to data coordinates based on the current transform matrix (from panning).
     * Basically, remove the effects of the transform matrix from the coordinates.
     * We need this because user mouse events give coordinates that include the transform, since that's what the user sees.
     * But we need to set internal coordinates that are based on a non-transformed coordinate field, since the transform matrix is the final layer of presentation, and shouldn't be contained in the lower level of data.
     *
     * @param {number|object} x_or_object - the x coordinate, or an object containing the x and y coordinate
     * @param {number} [y] - the y coordinate
     * @returns {object} - x and y coordinates after accounting for transform matrix
     */
     visualToActualCoordinates: function(x_or_obj,y) {
        if (this.checkType(x_or_obj, 'object')) {
            x = Syntree.Lib.checkArg(x_or_obj.x, 'number');
            y = Syntree.Lib.checkArg(x_or_obj.y, 'number');
        } else {
            x = Syntree.Lib.checkArg(x_or_obj, 'number');
            y = Syntree.Lib.checkArg(y, 'number');
        }

        var t = Syntree.Workspace.page.getTransform();
        x = x - t.dx;
        y = y - t.dy;

        return {
            x: x,
            y: y,
        }
    },

    /**
     * Extend parent class onto sub class, for instance.
     *
     * @param {function} parentConstructor - constructor function for the parent class
     * @param {function} subConstructor - constructor function for the sub class
     * @param {object} instance - the object instance being produced by subConstructor
     */
    extend: function(parentConstructor, subConstructor, instance) {
        subConstructor.prototype.__proto__ = parentConstructor.prototype;
        parentConstructor.call(instance);
    }

}

test_genId = function(n) {
    var gend = [];
    for (i=0; i<=n; i++) {
        var id = Syntree.Lib.genId();
        if (gend.indexOf(id) > -1) {
            console.log('duplicate: ' + id);
        }
        gend.push(id);
    }
    console.log(gend)
}
time_function = function(f,o) {
    var start_time = new Date().getTime();
    o[f]();
    var end_time = new Date().getTime();
    return end_time - start_time;
}

time_make_child = function(id,n) {
    Syntree.Workspace.page.select(Syntree.Workspace.page.allElements[id]);
    var times = [];
    var i = 0;
    while (i < n) {
        console.log('timing');
        times.push(time_function('_eventDown', Syntree.Workspace));
        Syntree.Workspace._eventUp();
        i++;
    }
    var sum = times.reduce(function(a, b) { return a + b; });
    return sum / times.length;
}

time_make_sibling = function(id,n) {
    Syntree.Workspace.page.select(Syntree.Workspace.page.allElements[id]);
    var times = [];
    var i = 0;
    while (i < n) {
        console.log('timing');
        times.push(time_function('_eventLeft', Syntree.Workspace));
        e = {
            ctrlKey: false,
        };
        Syntree.Workspace._eventRight(e);
        i++;
    }
    var sum = times.reduce(function(a, b) { return a + b; });
    return sum / times.length;
}