Source: src/main/javascript/oracle/oj/ojcube/Cube.js

Oracle® JavaScript Extension Toolkit (JET)
1.1.2

E65298-01

/**
 * Copyright (c) 2015, Oracle and/or its affiliates.
 * All rights reserved.
 */
/*jslint browser: true*/


/**
 * @class oj.Cube 
 * @classdesc Functions implemented by oj.DataColumnCube and oj.DataValueAttributeCube
 * @see oj.DataColumnCube
 * @see oj.DataValueAttributeCube
 * @since 1.1.0
 */

/**
 * @constructor
 * @export
 * @private
 */
 oj.Cube = function(rowset, layout)
{
    this.Init();
    
    // Wrap the incoming data in the appropriate interface
    this._rows = rowset;
    this._layout = /** @type {Array.<{axis,levels}>} */ (layout);

    this.BuildCube();
};

// Subclass from oj.Object 
oj.Object.createSubclass(oj.Cube, oj.Object, "oj.Cube");

/**
 * Initializes instance with the set options
 * @private
 */
oj.Cube.prototype.Init = function() 
{
  oj.Cube.superclass.Init.call(this);
};

/**
 * Get the oj.CubeAxis objects in this cube
 * @return {Array} an array of oj.CubeAxis objects
 * @export
 */
oj.Cube.prototype.getAxes = function() {
    // Generate axes list as union of subcube axes for particular page + page axes
    var cube = this._getPinnedCube();
    var axes = [];
    Array.prototype.push.apply(axes, cube ? cube.getAxes() : this._axes);
    // Add on the page axes
    for (var i = 2; i < this._axes.length; i++) {
        axes.push(this._axes[i]);
    }
    return axes;
};

oj.Cube.prototype._getNonPageLayout = function() {
    return this._axes;
};
 
/**
 * Get oj.CubeDataValues from this cube.  These represent the values of the data in the "body" of the cube
 *
 * @param {Array} indices an axis-ordered array of Objects or numbers.  If Objects, each should contain a 'start' property
 * (the zero based start index for the axis) and a 'count' representing the number of data values beginning at 'start' to return on this axis. 
 * This format allows the retrieval of a block of data.  Passing an array of numbers alone is equivalent to passing {start:<index>, count:1} and getting a single
 * oj.CubeDataValue
 * @return {Array|Object} either an array of arrays of oj.CubeDataValue, depending on the number of values requested in indices, or a single oj.CubeDataValue
 *                        The first subscript represents the 0th axis' values, and so on.
 * @export
 */
oj.Cube.prototype.getValues = function(indices) {
    // Get pinned cube
    var cube = this._getPinnedCube();
    
    // Normalize the argument to be objects with start/count properties
    var ind = cube._normalizeIndices(indices);

    var origRet = cube._walkIndex(ind, 0, [], []);
    var val = origRet;
    // Unpack single values tucked in arrays
    while (Array.isArray(val) && val.length === 1) {
        val = val[0];
        if (!Array.isArray(val)) {
            // Found an object in the middle--return it
            return val;
        }
    }
    return origRet;
};

/**
 * Set a pinned index for all axes above axis 1 ("pages")
 * @param {Array} pin an array of objects containing an integer 'axis' attribute and its corresponding 'index' value (to which to pin the cube)
 */
oj.Cube.prototype.setPage = function(pin) {
    // Make sure it's an array
    if (pin instanceof Array) {
        this._pin = pin;
    }
    else {
        this._pin = [pin];
    }
};
    
/**
 * 
 * @param {number} axisFrom the axis from which to move a level
 * @param {number} levelFrom the level within axisFrom to move to axisTo/levelTo (zero is slowest/outermost)
 * @param {number} axisTo the axis to which to move levelFrom
 * @param {number} levelTo the level within axisTo to move the levelFrom level (zero is slowest/outermost)
 * @param {oj.Cube.PivotType} type the type of pivot to perform
 * @returns {boolean} true if successful
 * @export
 */
oj.Cube.prototype.pivot = function(axisFrom, levelFrom, axisTo, levelTo, type) {
    var layout = this._layout;
    var axisFromObj = this._findAxisInLayout(axisFrom);
    // No from axis found: can't pivot
    if (!axisFromObj) {
        return false;
    }
    var axisToObj = null;
    if (axisTo < layout.length) {
        axisToObj = this._findAxisInLayout(axisTo);
    }
    else {
        // Add a new axis
        axisToObj = {'axis':axisTo, 'levels':[]};
        layout.push(axisToObj);
    }
    var levelsFrom = axisFromObj['levels'];
    var levelsTo = axisToObj['levels'];
    var levelFromObj = levelFrom < levelsFrom.length ? levelsFrom[levelFrom] : null;
    // No from level here, can't pivot
    if (!levelFromObj) {
        return false;
    }
    
    // Find where to move 
    if (levelTo >= levelsTo.length) {
        // beyond end--just add it.  after, before, swap doesn't matter
        levelsTo.push(levelFromObj);
        // Remove it from the old location
        levelsFrom.splice(levelFrom, 1);
    }
    else {
        if (type === oj.Cube.PivotType['SWAP']) {
            // Swap the elements
            levelsFrom[levelFrom] = levelsTo[levelTo];
            levelsTo[levelTo] = levelFromObj;
        }
        else {
            if (type === oj.Cube.PivotType['AFTER']) {
                // Splice operates as a "before" loc
                levelTo++;
            }
            // Insert the from obj into the to
            levelsTo.splice(levelTo, 0, levelFromObj);
            if (levelsTo === levelsFrom && levelTo < levelFrom) {
                // Relocate the from location, because the to splice shifted it (the from was in the same axis as the to, and was after the to)
                levelFrom++;
            }
            
            // On a move, remove the from object from the old axis
            levelsFrom.splice(levelFrom, 1);
        }
    }
    
    // Rebuild using the altered layout
    this.BuildCube();
    return true;
};

// Return the entry in the layout arg that represents axis #
oj.Cube.prototype._findAxisInLayout = function(axis) {
    for (var i = 0; i < this._layout.length; i++) {
        if (this._layout[i]['axis'] === axis) {
            return this._layout[i];
        }
    }
    return null;
};
    
/**
 * Return the current layout used to build the cube
 * 
 * @returns {Array} current layout
 * @see oj.Cube
 * @export
 */
oj.Cube.prototype.getLayout = function() {
    return this._layout;
};

/**
 * Valid pivot types
 * @enum {string}
 * @export
 */
oj.Cube.PivotType = {
    /**
     * Move the from location before the to location
     */
    'BEFORE' : "before",
    /**
     * Move the from location after the to location
     */
    'AFTER' : "after",
    /**
     * Exchange the from location with the to location
     */
    'SWAP' : "swap"    
};


oj.Cube.prototype._walkIndex = function(indices, depth, location, returnValue) {
   if (indices.length === 0) {
       // Get the current value at location
       var loc = location.slice(0);
       //loc.reverse();
       return this._getValue(loc);
   }
   else {
       var remaining = indices.slice(1);
       var start = indices[0].start;
       var count = indices[0].count;
       for (location[depth] = start; location[depth] < start+count; location[depth]++) {
           returnValue.push(this._walkIndex(remaining, depth+1, location, []));
       }
   }
   return returnValue;
};

// Make sure all start/count data is filled in and within range
oj.Cube.prototype._normalizeIndices = function(indices) {
    var ind = [];
    if (!indices) {
        return ind;
    }
    var numAxes = Math.min(indices.length, this._axes.length);
    for (var a = 0; a < numAxes; a++) {        
        var index = indices[a];
        if (index instanceof Object && (index.hasOwnProperty('start') || index.hasOwnProperty('count'))) {
            if (index.hasOwnProperty('start')) {
                if (index.hasOwnProperty('count')) {
                    ind.push(this._generateIndex(index.start, index.count, a));
                }
                else {
                    ind.push(this._generateIndex(index.start, 1, a));
                }
            }
            else {
                // Must have count
                ind.push(this._generateIndex(0, index.count, a));
            }
        }
        else {
            // Convert the number to an object
            ind.push(this._generateIndex(index, 1, a));
        }
    }
    return ind;
};
    
oj.Cube.prototype._generateIndex = function(start, count, axis) {
    // Get true count
    var trueCount = this.getAxes()[axis].getExtent();
    
    if (start >= trueCount || start < 0) {
        start = 0;
    }
    count = Math.min(count,trueCount-start);
            
    return {start: start, index: start, count: count};
};

oj.Cube.prototype._getValue = function(indices) {
    // indices is just an axis-ordered array of locations for which to get the value
    // Should only be one--only one data value here
    var key = this._createCubeKeys(indices);
    if (key) {
        var hash = key.GetHashCodes();
        if (hash.length > 0) {
            var obj = this._data[hash[0].key];
            if (obj) {
                return new oj.CubeDataValue(obj.value, indices, obj.aggType, obj.rows, obj.square);
            }
        }
    }
    return new oj.CubeDataValue(null, indices, undefined, []);
};
    
// Generate the axes from the layout
oj.Cube.prototype.GenerateAxes = function() {    
    var pageLayout = this._getPageLayout();
    // If we have pages--only generate those for this cube, and set up the default 0th pin    
    this._pin = [];
    for (var i = 0; i < pageLayout.length; i++) {
        this._getAxis(pageLayout[i]['axis'], pageLayout[i]['levels']);
        this._pin.push({'axis':pageLayout[i]['axis'], 'index':0});
    }
    if (pageLayout.length === 0) {
        // Do remaining axes if any (row and column)
        var nonPageLayout = this._getNonPageLayout();
        for (var i = 0; i < nonPageLayout.length; i++) {
            this._getAxis(nonPageLayout[i]['axis'], nonPageLayout[i]['levels']);
        }
    }
};

oj.Cube.prototype._getPageLayout = function() {
    var pageOnlyLayout = [];
    for (var i = 0; i < this._layout.length; i++) {
        var axis = this._layout[i]['axis'];
        if (axis > 1) {
            pageOnlyLayout.push(this._layout[i]);
        }
    }
    return pageOnlyLayout;
};

oj.Cube.prototype._getNonPageLayout = function() {
    var nonPageLayout = [];
    for (var i = 0; i < this._layout.length; i++) {
        var axis = this._layout[i]['axis'];
        if (axis < 2) {
            nonPageLayout.push(this._layout[i]);
        }
    }
    return nonPageLayout;
};

oj.Cube.prototype.BuildCube = function() {
    this._axes = [];
    
    // Associative array for data
    this._data = [];
    
    // Associative array for cubes
    this._cubes = [];
    
    this.GenerateAxes();
    
    // Can start a cube with no data--add on from "parent"
    if (this._rows === null) {
        return;
    }
        
    // Walk each line of the rowset, and distribute its attributes to the axes and the data store
    for (var row = 0; row < this._rows.length; row++) {
        // Generate a "page key"
        var pageKey = new oj.CubeKeys();
        for (var axis = 2; axis < this._axes.length; axis++) {
            pageKey = this._axes[axis].ProcessRow(this._rows[row], pageKey);
        }
        // Grab the data value from the page if it's there
        var pageHashObj = pageKey.GetHashCodes();
        // See if we have a cubes for these pageKeys
        for (var ph = 0; ph < pageHashObj.length; ph++) {
            var pageHash = pageHashObj[ph].key;
            var cube = this._cubes[pageHash];
            if (!cube) {
                // Generate a new one
                cube = this._cubes[pageHash] = this.GenerateCube(this._getNonPageLayout());
            }
            // Send the row through the lower axes
            var keys = new oj.CubeKeys();
            var maxAxes = cube._axes.length;
            for (var axis = 0; axis < maxAxes; axis++) {
                keys = cube._axes[axis].ProcessRow(this._rows[row], keys);
            }
            // Convert the retrieved keys->data into data storage
            var hash = keys.GetHashCodes();
            // Must account for data value coming from page, possibly...pass in the hash that has the data
            var dataHash = hash;
            if (pageHashObj[ph].dataValue !== undefined) {
                dataHash = [];
                // Must be in an array
                for (var h = 0; h < hash.length; h++) {
                    dataHash.push(pageHashObj[ph]);
                }
            }
            cube._storeData(hash, dataHash, this._rows[row]);
        }
    }
};

oj.Cube.prototype._storeData = function(hash, dataHash, row) {
    for (var i = 0; i < hash.length; i++) {
        this._data[hash[i].key] = this._aggregate(dataHash[i], this._data[hash[i].key], row);
    }
};


oj.Cube.prototype._getPinnedCube = function() {
    return this._cubes[this._getHashFromPin(this._pin)];
};
        
oj.Cube.prototype._getHashFromPin = function(pin) {
    var keys = new oj.CubeKeys();
    if (pin && pin.length > 0) {
        // Sort by axis attribute
        pin.sort(function(a,b) {
            return a['axis'] - b['axis'];
        });
        // Translate the axis/index pin to hash key
        var axes = this._axes;
        for (var i = 0; i < pin.length; i++) {
            keys = axes[pin[i]['axis']].GetCubeKeys(pin[i]['index'], keys);
        }
    }
    return keys.GetHashCodes()[0].key;
};
    
oj.Cube._isValid = function(value) {
    if (value) {
        return value.value !== undefined && value.value !== null;
    }
    return false;
};

oj.Cube.prototype._createAggValue = function(value, aggType, rows, row, props) {
    rows.push(row);
    var retObj = {};
    for (var p in props) {
        if (props.hasOwnProperty(p)) {
            retObj[p] = props[p];
        }
    }
    retObj.value = value;
    retObj.aggType = aggType;
    retObj.rows = rows;
    return retObj;
};

// Don't treat strings as numbers, ever
oj.Cube._isNumber = function(value) {
    if (oj.StringUtils.isString(value.value)) {
        return false;
    }
    // String versions of numbers return false for isNaN--we want to treat them as non numbers always
    return !isNaN(value.value);
};

oj.Cube.prototype._aggregate = function(hash, currValue, row) {
    var aggObj = this.GetAggType(hash.dataValue);
    var aggType = /** @type {Object} */(aggObj.aggregation);
    var validCurr = oj.Cube._isValid(currValue);
    var validHash = oj.Cube._isValid(hash);
    var isNumCurr = validCurr && oj.Cube._isNumber(currValue);
    var isNumHash = validHash && oj.Cube._isNumber(hash);
    switch (aggType) {
        case oj.CubeAggType['SUM']:            
            if (validCurr && validHash) {
                if (isNumCurr && isNumHash) {
                    return this._createAggValue(currValue.value + hash.value, aggType, currValue.rows, row, {});
                }
                return this._createAggValue(NaN, aggType, currValue.rows, row, {});
            }
            if (validHash && !validCurr) {
                if (isNumHash) {
                    return this._createAggValue(hash.value, aggType, [], row, {});
                }
                return this._createAggValue(NaN, aggType, [], row, {});
            }
            return currValue;
        case oj.CubeAggType['AVERAGE']:
            if (validCurr && validHash) {
                if (isNumCurr && isNumHash) {
                    return this._createAggValue((currValue.sum + hash.value) / (currValue.rows.length+1), aggType, currValue.rows, row, {sum:(currValue.sum + hash.value)});
                }
                return this._createAggValue(NaN, aggType, currValue.rows, row, {sum:currValue.sum});
            }
            if (validHash && !validCurr) {
                if (isNumHash) {
                    return this._createAggValue(hash.value, aggType, [], row, {sum:hash.value});
                }
                return this._createAggValue(NaN, aggType, [], row, {sum:NaN});
            }
            return currValue;
        case oj.CubeAggType['VARIANCE']:
        case oj.CubeAggType['STDDEV']:
            if (validCurr && validHash) {
                if (isNumCurr && isNumHash) {
                    var newCount = currValue.rows.length+1;
                    var avg = currValue.value + (hash.value - currValue.value) / newCount;
                    return this._createAggValue(avg, aggType, currValue.rows, row, {square:(currValue.square + (hash.value - currValue.value) * (hash.value - avg))});
                }
                return this._createAggValue(NaN, aggType, currValue.rows, row, {square:NaN});
            }
            if (validHash && !validCurr) {
                if (isNumHash) {
                    return this._createAggValue(hash.value, aggType, [], row, {square:0});
                }
                return this._createAggValue(NaN, aggType, [], row, {square:NaN});
            }
            return currValue;
        case oj.CubeAggType['NONE']:
            return this._createAggValue(null, aggType, validCurr ? currValue.rows : [], row, {});
        case oj.CubeAggType['FIRST']:
            if (validCurr) {
                return this._createAggValue(currValue.value, aggType, currValue.rows, row, {});
            }
            if (validHash) {
                return this._createAggValue(hash.value, aggType, [], row, {});
            }
            return currValue;
        case oj.CubeAggType['MIN']:
            if (validCurr && validHash) {
                if (isNumCurr && isNumHash) {
                    return this._createAggValue(Math.min(currValue.value, hash.value), aggType, currValue.rows, row, {});
                }
                return this._createAggValue(NaN, aggType, currValue.rows, row, {});
            }
            if (validHash && !validCurr) {       
                if (isNumHash) {
                    return this._createAggValue(hash.value, aggType, [], row, {});
                }
                return this._createAggValue(NaN, aggType, [], row, {});
            }
            return currValue;
        case oj.CubeAggType['MAX']:
            if (validCurr && validHash) {
                if (isNumCurr && isNumHash) {
                    return this._createAggValue(Math.max(currValue.value, hash.value), aggType, currValue.rows, row, {});
                }
                return this._createAggValue(NaN, aggType, currValue.rows, row, {});
            }
            if (validHash && !validCurr) {       
                if (isNumHash) {
                    return this._createAggValue(hash.value, aggType, [], row, {});
                }
                return this._createAggValue(NaN, aggType, [], row, {});
            }
            return currValue;
        case oj.CubeAggType['COUNT']:
            if (validCurr && validHash) {
                return this._createAggValue(currValue.value+1, aggType, currValue.rows, row, {});
            }
            if (validHash && !validCurr) {
                return this._createAggValue(1, aggType, [], row, {});
            }
            return currValue;
        case oj.CubeAggType['CUSTOM']:
            var callback = aggObj.callback;
            var val = callback.call(this, validCurr ? currValue.value : undefined, validHash ? hash.value : undefined);
            return this._createAggValue(val, aggType, validCurr ? currValue.rows : [], row, {});
    }
};

// Get or create the given axis
oj.Cube.prototype._getAxis = function(axis, levels) {
    if (axis >= this._axes.length) {
        // Must add on enough to cover
        var newElems = new Array(axis-this._axes.length+1);
        Array.prototype.push.apply(this._axes, newElems);
    }

    // Initialize it if blank
    if (!this._axes[axis]) {
        this._axes[axis] = new oj.CubeAxis(levels, axis, this);
    }
    
    return this._axes[axis];
};


// Create a key object from an axis-ordered array of slowest-to-fastest arrays of CubeAxisValues
oj.Cube.prototype._createCubeKeys = function(indices) {
    var axes = this.getAxes();
    var keys = new oj.CubeKeys();
    for (var a = 0; a < indices.length; a++) {
        keys = axes[a].GetCubeKeys(indices[a], keys);
    }
    return keys;
};
    
    
oj.Cube.prototype.ProcessLevel = function(levels, levelNum, currNode, row, keys, addKeys) {
    oj.Assert.failedInAbstractFunction();
};

oj.Cube.prototype.GenerateCube = function(layout) {
    oj.Assert.failedInAbstractFunction();
};

oj.Cube.prototype.GenerateLevel = function(level, axis) {    
    oj.Assert.failedInAbstractFunction();    
};

/**
 * @param {Object} dataValue
 * @returns {Object}
 * @private
 */
oj.Cube.prototype.GetAggType = function(dataValue) {
    oj.Assert.failedInAbstractFunction();
    return {};
};