| //////////////////////////////////////////////////////////////////////////////// |
| // |
| // Licensed to the Apache Software Foundation (ASF) under one or more |
| // contributor license agreements. See the NOTICE file distributed with |
| // this work for additional information regarding copyright ownership. |
| // The ASF licenses this file to You under the Apache License, Version 2.0 |
| // (the "License"); you may not use this file except in compliance with |
| // the License. You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| // |
| //////////////////////////////////////////////////////////////////////////////// |
| |
| |
| package mx.olap |
| { |
| |
| import flash.utils.Dictionary; |
| |
| import mx.collections.IList; |
| |
| /** |
| * @private |
| */ |
| public class CubeNodeBuilder |
| { |
| include "../core/Version.as"; |
| |
| //-------------------------------------------------------------------------- |
| // |
| // Variables |
| // |
| //-------------------------------------------------------------------------- |
| |
| private var prevNodeAtLevel:Array; // of CubeNodes |
| private var allNodeAtLevel:Array; // of CubeNodes |
| private var prevValueAtLevel:Array; |
| |
| // the level at which the new property is going to be placed |
| private var currentLevel:int; |
| |
| private var prevLevel:int; |
| private var nextLevel:int; |
| |
| //the nodes whose values are going to be aggregated as the above node |
| //value has changed. |
| private var closingNodesBelow:Array; |
| private var closingValues:Array; |
| |
| //list of measures in the cube |
| private var measureMembers:IList; |
| private var measuresLength:int ; |
| |
| /** |
| * @private |
| * Maps containing nodes which need to be passed to the aggrgator |
| * to compute the final result. The measure object is used as a key. |
| * |
| */ |
| // simple aggregation map |
| private var computeEndMap:Dictionary = new Dictionary(false); |
| |
| //aggregation of aggregations map |
| private var computeObjEndMap:Dictionary = new Dictionary(false); |
| |
| private var measureMap:Object = {}; |
| |
| /** |
| * @private |
| * An array of properites in the rootNode. |
| * This is used to aggregate each property asychronously |
| * to avoid player time out. |
| * |
| */ |
| private var rootCellValues:Array; |
| |
| //-------------------------------------------------------------------------- |
| // |
| // Properties |
| // |
| //-------------------------------------------------------------------------- |
| |
| private var _cube:OLAPCube; |
| |
| public function set cube(c:IOLAPCube):void |
| { |
| _cube = c as OLAPCube; |
| } |
| |
| /** |
| * The top most node of the cube. |
| * All navigations begin from this node. |
| * |
| * @langversion 3.0 |
| * @playerversion Flash 9 |
| * @playerversion AIR 1.1 |
| * @productversion Flex 3 |
| */ |
| public var rootNode:CubeNode; |
| |
| /** |
| * The property name used to refer to the property |
| * pointing at the aggregation of nodes below a CubeNode |
| * |
| * @langversion 3.0 |
| * @playerversion Flash 9 |
| * @playerversion AIR 1.1 |
| * @productversion Flex 3 |
| */ |
| public var allNodePropertyName:String ="(All)"; |
| |
| /** |
| * Indicates the last level of data other than measures. |
| * This is used to read the measure values from the data item |
| * when processing reaches this level. |
| * |
| * @langversion 3.0 |
| * @playerversion Flash 9 |
| * @playerversion AIR 1.1 |
| * @productversion Flex 3 |
| */ |
| public var levelPreviousToMeasuresIndex:int; |
| |
| //total number of nodes in the cube. (used for perf improvement analysis) |
| //private var totalNodes:int; |
| |
| //count of cubes collapsed (used for perf improvement analysis) |
| //private var nodesUpdated:int; |
| |
| //-------------------------------------------------------------------------- |
| // |
| // Methods |
| // |
| //-------------------------------------------------------------------------- |
| |
| /** |
| * Starts the process of building the cube by initializing |
| * the members. |
| * |
| * @langversion 3.0 |
| * @playerversion Flash 9 |
| * @playerversion AIR 1.1 |
| * @productversion Flex 3 |
| */ |
| public function initNodeBuilding():void |
| { |
| prevNodeAtLevel = []; |
| allNodeAtLevel = []; |
| prevValueAtLevel = []; |
| |
| //compute for all the measures |
| measureMembers = _cube.findDimension("Measures").members; |
| |
| measuresLength = measureMembers.length; |
| for each (var measure:OLAPMeasure in measureMembers) |
| { |
| computeEndMap[measure] = []; |
| computeObjEndMap[measure] = []; |
| measureMap[measure.name] = measure; |
| } |
| } |
| |
| /** |
| * Instructs the builder to start processing the next data item. |
| * |
| * @langversion 3.0 |
| * @playerversion Flash 9 |
| * @playerversion AIR 1.1 |
| * @productversion Flex 3 |
| */ |
| public function moveToNextRound():void |
| { |
| currentLevel = 0; |
| prevLevel = -1; |
| nextLevel = 1; |
| } |
| |
| /** |
| * Finalizes building the cube by computing aggregations at all nodes. |
| * |
| * @langversion 3.0 |
| * @playerversion Flash 9 |
| * @playerversion AIR 1.1 |
| * @productversion Flex 3 |
| */ |
| public function completeNodeBuilding():Boolean |
| { |
| if (prevNodeAtLevel && prevValueAtLevel) |
| { |
| // close all nodes pending |
| //TODO initializing rootNode twice |
| rootNode = prevNodeAtLevel[0]; |
| closingNodesBelow = prevNodeAtLevel.reverse(); |
| closingValues = prevValueAtLevel.reverse(); |
| |
| var tempLevel:int = closingNodesBelow.length - 1; |
| var n:int = closingNodesBelow.length; |
| for (var i:int = 0; i < n; ++i) |
| { |
| if (tempLevel < 1) |
| continue; |
| var closingValue:Object = closingValues[i]; |
| //OLAPTrace.traceMsg("Closing value:" + closingValues[i], //OLAPTrace.TRACE_LEVEL_2); |
| var closingNode:Object = closingNodesBelow[i]; |
| if (closingNode[closingValue] is CubeNode) |
| { |
| if (closingNode[allNodePropertyName] is CubeNode) |
| { |
| //if there are is only one property (other than (all) ) we short circuit |
| if (closingNode.numCells == 2) |
| closingNode[allNodePropertyName] = closingNode[closingValue]; |
| else |
| accumValuesFromNode(closingNode[allNodePropertyName], closingNode[closingValue]); |
| } |
| } |
| |
| --tempLevel; |
| } |
| |
| prevNodeAtLevel = null; |
| prevValueAtLevel = null; |
| allNodeAtLevel = null; |
| |
| rootCellValues = []; |
| for (var rootCell:String in rootNode) |
| { |
| if (rootCell == allNodePropertyName) |
| continue; |
| rootCellValues.push(rootCell); |
| } |
| return false; |
| } |
| |
| //aggregate the top node |
| while(rootCellValues.length) |
| { |
| rootCell = rootCellValues.pop(); |
| // can we accumulate everything from the tree to the all tree here? |
| accumValuesFromNode(rootNode[allNodePropertyName], rootNode[rootCell]); |
| return false; |
| } |
| |
| //finalize all aggregation computation |
| var temp:Array; |
| var y:Object; |
| var measure:OLAPMeasure; |
| var aggregator:IOLAPCustomAggregator; |
| for (var x:Object in computeEndMap) |
| { |
| measure = x as OLAPMeasure; |
| aggregator = measure.aggregator as IOLAPCustomAggregator; |
| temp = computeEndMap[x]; |
| for each (y in temp) |
| { |
| y["saved_" + measure.name] = y[measure.name]; |
| y[measure.name] = aggregator.computeEnd(y[measure.name], measure.dataField); |
| } |
| delete computeEndMap[x]; |
| } |
| |
| for (x in computeObjEndMap) |
| { |
| measure = x as OLAPMeasure; |
| aggregator = measure.aggregator as IOLAPCustomAggregator; |
| temp = computeObjEndMap[x]; |
| for each (y in temp) |
| { |
| y["saved_"+measure.name] = y[measure.name]; |
| y[measure.name] = aggregator.computeObjectEnd(y[measure.name], measure.dataField); |
| } |
| delete computeObjEndMap[x]; |
| } |
| |
| computeEndMap = new Dictionary(false); |
| computeObjEndMap = new Dictionary(false); |
| |
| //collapse CubeNodes which have only one property |
| //The aggregation property would point to the single property itself |
| optimizeCube(); |
| |
| return true; |
| } |
| |
| /** |
| * Adds a new value to the cube. If the value is different from the previous value |
| * at the current level adds the new value as a property to the CubeNode and creates |
| * a new CubeNode below. Also creates a CubeNode for aggregating the properties in the |
| * new CubeNode. |
| * |
| * @langversion 3.0 |
| * @playerversion Flash 9 |
| * @playerversion AIR 1.1 |
| * @productversion Flex 3 |
| */ |
| public function addValueToNodeBuilder(values:Object, currentData:Object):void |
| { |
| var i:int = 0; |
| for each(var value:Object in values) |
| { |
| // decide whether to create a new node or use the previous node |
| var prevNode:CubeNode = prevNodeAtLevel[currentLevel]; |
| var prevValue:Object = prevValueAtLevel[currentLevel]; |
| var closingValue:Object; |
| var closingNode:Object |
| |
| if (prevValue) |
| { |
| if (prevValue == value) |
| { |
| // continue to process the same value |
| // update nodes |
| //OLAPTrace.traceMsg("Continue with value:" + prevValue, OLAPTrace.TRACE_LEVEL_2); |
| } |
| else |
| { |
| // no more processing of this value. |
| // time to compute update all nodes etc |
| |
| // close all nodes below this node |
| closingNodesBelow = prevNodeAtLevel.splice(nextLevel).reverse(); |
| closingValues = prevValueAtLevel.splice(nextLevel).reverse(); |
| var tempLevel:int = currentLevel + closingNodesBelow.length; |
| var closingNodeLength:int = closingNodesBelow.length; |
| for (i= 0; i < closingNodeLength; ++i) |
| { |
| if (tempLevel != 0) |
| { |
| closingValue = closingValues[i]; |
| //OLAPTrace.traceMsg("Closing value:" + closingValues[tempIndex], OLAPTrace.TRACE_LEVEL_2); |
| closingNode = closingNodesBelow[i]; |
| if (closingNode[closingValue] is CubeNode) |
| { |
| if (closingNode[allNodePropertyName] is CubeNode) |
| { |
| if (tempLevel > currentLevel && closingNode.numCells == 2) |
| closingNode[allNodePropertyName] = closingNode[closingValue]; |
| else |
| accumValuesFromNode(closingNode[allNodePropertyName], closingNode[closingValue]); |
| } |
| } |
| } |
| |
| --tempLevel; |
| } |
| |
| //OLAPTrace.traceMsg("Closing value:" + prevValue, //OLAPTrace.TRACE_LEVEL_2); |
| closingValue = prevValue; |
| closingNode = prevNode; |
| if (currentLevel != 0 && closingNode[closingValue] is CubeNode) |
| { |
| if (closingNode[allNodePropertyName] is CubeNode) |
| accumValuesFromNode(closingNode[allNodePropertyName], closingNode[closingValue]); |
| } |
| |
| //OLAPTrace.traceMsg("New value:" + value, //OLAPTrace.TRACE_LEVEL_2); |
| } |
| } |
| |
| |
| if (!prevNode) |
| { |
| //OLAPTrace.traceMsg("Creating new node: " + value, //OLAPTrace.TRACE_LEVEL_2); |
| // - create a new node, put the pointer to the ALL node, add new key to the ALL node |
| var newNode:CubeNode = new CubeNode(currentLevel); |
| |
| // create a all node to summaries the cells of this node |
| newNode[allNodePropertyName] = new CubeNode(currentLevel+1); |
| ++newNode.numCells; |
| |
| //TODO initializing rootNode twice |
| if (currentLevel == 0 && prevNodeAtLevel.length == 0) |
| rootNode = newNode; |
| |
| prevNodeAtLevel[currentLevel] = newNode; |
| |
| var allNode:CubeNode = allNodeAtLevel[currentLevel]; |
| if (!allNode) |
| { |
| // for the top most level we would have only one node |
| // which is also the all node |
| // for other levels we have one node which is a all node |
| // containing aggr value of all cells in the nodes at that level |
| if (currentLevel > 0) |
| { |
| allNodeAtLevel[currentLevel] = allNode = new CubeNode(currentLevel); |
| allNode[allNodePropertyName] = {}; |
| allNodeAtLevel[prevLevel][allNodePropertyName] = allNode; |
| ++allNode.numCells; |
| } |
| else |
| { |
| // special case of zero level |
| allNodeAtLevel[currentLevel] = newNode; |
| } |
| } |
| |
| prevNode = newNode; |
| } |
| else |
| { |
| // add a new cell/property to the previous node if it doesn't exist |
| if (!prevNode.hasOwnProperty(value)) |
| { |
| prevNodeAtLevel.splice(nextLevel); |
| prevValueAtLevel.splice(nextLevel); |
| } |
| } |
| |
| // are we at the last level? |
| // if so we take up measures computation |
| if (currentLevel == levelPreviousToMeasuresIndex) |
| { |
| if (!prevNode.hasOwnProperty(value)) |
| { |
| prevNode[value] = new SummaryNode; |
| ++prevNode.numCells; |
| } |
| var temp:Object; |
| if (prevNode[allNodePropertyName] is CubeNode || prevNode[allNodePropertyName] is Number) |
| { |
| temp = prevNode[allNodePropertyName] = new SummaryNode; |
| } |
| else |
| { |
| temp = prevNode[allNodePropertyName]; |
| } |
| |
| for (i = 0; i < measuresLength; ++i) |
| { |
| var measureMember:OLAPMember = measureMembers.getItemAt(i) as OLAPMember; |
| var newDataValue:Object = currentData[measureMember.dataField]; |
| |
| //force convertion of measure value to a number. |
| //xml dataType runs into problems with value of 0 |
| if (typeof(newDataValue) == "xml") |
| newDataValue = Number(newDataValue.toString()); |
| newDataValue = Number(newDataValue); |
| addValueToNode(prevNode[value], measureMember.name, newDataValue, measureMember as OLAPMeasure, currentData); |
| addValueToNode(temp, measureMember.name, newDataValue, measureMember as OLAPMeasure, currentData); |
| } |
| } |
| |
| // make the node at previous level point to the node |
| if (currentLevel > 0) |
| { |
| var prevLevelNode:Object = prevNodeAtLevel[prevLevel]; |
| if (!prevLevelNode.hasOwnProperty(prevValueAtLevel[prevLevel])) |
| { |
| var prevLevelValue:Object = prevValueAtLevel[prevLevel]; |
| prevLevelNode[prevLevelValue] = newNode; |
| ++prevLevelNode.numCells; |
| } |
| } |
| |
| prevValueAtLevel[currentLevel] = value; |
| ++prevLevel; |
| ++currentLevel; |
| ++nextLevel; |
| } |
| } |
| |
| /** |
| * @private |
| */ |
| private function optimizeCube():void |
| { |
| var total:int =0; |
| //totalNodes = 0; |
| //nodesUpdated = 0; |
| //start from root node and count the number of nodes which have only one cell. |
| //skip the allValue because we are not updating the numCells there. |
| if (rootNode) |
| total = findNodeWithOneCell(rootNode); |
| //trace(nodesUpdated + "/" + total + "/" + totalNodes); |
| } |
| |
| /** |
| * @private |
| * Recursively collapses CubeNodes |
| * |
| */ |
| private function findNodeWithOneCell(node:CubeNode):int |
| { |
| //++totalNodes; |
| var total:int = 0; |
| if (node.numCells == 2) |
| ++total; |
| for (var p:String in node) |
| { |
| //if (x == allNodePropertyName) |
| // continue; |
| if (node.numCells == 2) |
| { |
| if (node[allNodePropertyName] != node[p]) |
| { |
| delete node[allNodePropertyName]; |
| node[allNodePropertyName] = node[p]; |
| //++nodesUpdated; |
| } |
| } |
| |
| var cNode:CubeNode = node[p] as CubeNode; |
| if (cNode) |
| { |
| total += findNodeWithOneCell(cNode); |
| } |
| } |
| return total; |
| } |
| |
| /** |
| * @private |
| * Adds a measure value to the node. |
| * Invokes aggregator functions to compute the value. |
| * |
| */ |
| private function addValueToNode(node:Object, name:String, |
| value:Object, measure:OLAPMeasure, rowData:Object):void |
| { |
| var aggregator:IOLAPCustomAggregator; |
| |
| //if the value is a number we need to compute the summary. |
| if (value is Number) |
| { |
| aggregator = measure.aggregator as IOLAPCustomAggregator; |
| if (!node.hasOwnProperty(name)) |
| { |
| node[name] = aggregator.computeBegin(measure.dataField); |
| computeEndMap[measure].push(node); |
| aggregator.computeLoop(node[name], measure.dataField, /*value,*/ rowData); |
| } |
| else |
| { |
| aggregator.computeLoop(node[name], measure.dataField, /*value,*/ rowData); |
| } |
| } |
| else |
| { |
| var temp:Object; |
| if (!node.hasOwnProperty(name)) |
| { |
| node[name] = temp = new SummaryNode; |
| ++node.numCells; |
| for (var p:String in value) |
| { |
| measure = measureMap[p]; |
| aggregator = measure.aggregator as IOLAPCustomAggregator; |
| if (temp.hasOwnProperty(p)) |
| { |
| aggregator.computeObjectLoop(temp[p], value[p]); |
| } |
| else |
| { |
| temp[p] = aggregator.computeObjectBegin(value[p]); |
| computeObjEndMap[measure].push(temp); |
| } |
| } |
| } |
| else |
| { |
| if (value is SummaryNode && !(node[name] is SummaryNode)) |
| { |
| var incr:Boolean = false; |
| if (!node.hasOwnProperty(name)) |
| incr = true; |
| temp = node[name] = new SummaryNode; |
| if (incr) |
| ++node.numCells; |
| } |
| else |
| { |
| temp = node[name]; |
| } |
| |
| for (p in value) |
| { |
| measure = measureMap[p]; |
| aggregator = measure.aggregator as IOLAPCustomAggregator; |
| if (temp.hasOwnProperty(p)) |
| { |
| aggregator.computeObjectLoop(temp[p], value[p]); |
| } |
| else |
| { |
| temp[p] = aggregator.computeObjectBegin(value[p]); |
| computeObjEndMap[measure].push(temp); |
| } |
| } |
| } |
| } |
| } |
| |
| /** |
| * @private |
| * Aggregates all property values from source to target objects recursively. |
| * |
| */ |
| private function accumValuesFromNode(target:Object, |
| source:Object):void |
| { |
| for (var p:String in source) |
| { |
| if (p == allNodePropertyName) |
| { |
| continue; |
| } |
| var value:Object = source[p]; |
| if (value is CubeNode) |
| { |
| var newNode:CubeNode; |
| if (target[p] is CubeNode) |
| { |
| newNode = target[p]; |
| } |
| else |
| { |
| target[p] = newNode = new CubeNode(value.level); |
| ++target.numCells; |
| } |
| accumValuesFromNode(newNode, value); |
| } |
| else |
| { |
| addValueToNode(target, p, value, null, null); |
| } |
| } |
| |
| if (target.numCells == 1) |
| { |
| for (p in target) |
| { |
| target[allNodePropertyName] = target[p]; |
| break; |
| } |
| ++target.numCells; |
| } |
| else if (target.numCells == 2 && target.hasOwnProperty(allNodePropertyName)) |
| { |
| for (p in target) |
| { |
| if (p == allNodePropertyName) |
| continue; |
| if (target[allNodePropertyName] != target[p]) |
| target[allNodePropertyName] = target[p]; |
| break; |
| } |
| } |
| else |
| { |
| var done:Boolean = false; |
| var q:String; |
| for (p in target) |
| { |
| if (p == allNodePropertyName) |
| continue; |
| if (target[allNodePropertyName] == target[p]) |
| { |
| value = target[p]; |
| if (value is CubeNode) |
| { |
| target[allNodePropertyName] = newNode = new CubeNode(/*value.level*/); |
| accumValuesFromNode(newNode, value); |
| for (q in target) |
| { |
| if (q == allNodePropertyName || |
| q == p) |
| continue; |
| //now we have the other property. |
| accumValuesFromNode(newNode, source[q]); |
| } |
| done = true; |
| } |
| else |
| { |
| target[allNodePropertyName] = null; |
| addValueToNode(target, allNodePropertyName, value, null, null); |
| for (q in target) |
| { |
| if (q == allNodePropertyName || |
| q == p) |
| continue; |
| //now we have the other property. |
| addValueToNode(target, allNodePropertyName, target[q], null, null); |
| } |
| done = true; |
| } |
| break; |
| } |
| } |
| |
| if (!done) |
| { |
| p = allNodePropertyName; |
| value = source[p]; |
| if (value is CubeNode) |
| { |
| if (target[p] is CubeNode) |
| newNode = target[p]; |
| else |
| { |
| target[p] = newNode = new CubeNode(value.level); |
| ++target.numCells; |
| } |
| accumValuesFromNode(newNode, value); |
| } |
| else |
| { |
| addValueToNode(target, p, value, null, null); |
| } |
| } |
| } |
| } |
| |
| } |
| } |