blob: cf7eab700346f8097231bac072f5df70e90507e8 [file] [log] [blame]
////////////////////////////////////////////////////////////////////////////////
//
// 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.collections
{
import flash.events.TimerEvent;
import flash.utils.Dictionary;
import flash.utils.Timer;
import flash.xml.XMLNode;
import mx.collections.errors.ItemPendingError;
import mx.core.mx_internal;
import mx.events.CollectionEvent;
import mx.events.CollectionEventKind;
import mx.utils.UIDUtil;
use namespace mx_internal;
//--------------------------------------
// Other metadata
//--------------------------------------
[DefaultProperty("grouping")]
/**
* The GroupingCollection2 class lets you create grouped data from flat data
* for display in the AdvancedDataGrid control.
* When you create the instance of the GroupingCollection2 from your flat data,
* you specify the field or fields of the data used to create the hierarchy.
*
* <p><b>Note: </b>In the previous release of Flex, you used the GroupingCollection class
* with the AdvancedDataGrid control.
* The GroupingCollection2 class is new for Flex 4 and provides better performance
* than GroupingCollection.</p>
*
* <p>To populate the AdvancedDataGrid control with grouped data,
* you create an instance of the GroupingCollection2 class from your flat data,
* and then pass that GroupingCollection2 instance to the data provider
* of the AdvancedDataGrid control.
* To specify the grouping fields of your flat data,
* you pass a Grouping instance to
* the <code>GroupingCollection2.grouping</code> property.
* The Grouping instance contains an Array of GroupingField instances,
* one per grouping field. </p>
*
* <p>The following example uses the GroupingCollection2 class to define
* two grouping fields: Region and Territory.</p>
*
* <pre>
* &lt;mx:AdvancedDataGrid id=&quot;myADG&quot;
* &lt;mx:dataProvider&gt;
* &lt;mx:GroupingCollection2 id=&quot;gc&quot; source=&quot;{dpFlat}&quot;&gt;
* &lt;mx:grouping&gt;
* &lt;mx:Grouping&gt;
* &lt;mx:GroupingField name=&quot;Region&quot;/&gt;
* &lt;mx:GroupingField name=&quot;Territory&quot;/&gt;
* &lt;/mx:Grouping&gt;
* &lt;/mx:grouping&gt;
* &lt;/mx:GroupingCollection2&gt;
* &lt;/mx:dataProvider&gt;
*
* &lt;mx:columns&gt;
* &lt;mx:AdvancedDataGridColumn dataField=&quot;Region&quot;/&gt;
* &lt;mx:AdvancedDataGridColumn dataField=&quot;Territory&quot;/&gt;
* &lt;mx:AdvancedDataGridColumn dataField=&quot;Territory_Rep&quot;/&gt;
* &lt;mx:AdvancedDataGridColumn dataField=&quot;Actual&quot;/&gt;
* &lt;mx:AdvancedDataGridColumn dataField=&quot;Estimate&quot;/&gt;
* &lt;/mx:columns&gt;
* &lt;/mx:AdvancedDataGrid&gt;
* </pre>
*
* @mxml
*
* The <code>&lt;mx.GroupingCollection2&gt;</code> inherits all the tag attributes of its superclass,
* and defines the following tag attributes:</p>
*
* <pre>
* &lt;mx:GroupingCollection2
* <b>Properties </b>
* grouping="<i>No default</i>"
* source="<i>No default</i>"
* summaries="<i>No default</i>"
* /&gt;
* </pre>
*
* @see mx.controls.AdvancedDataGrid
* @see mx.collections.Grouping
* @see mx.collections.GroupingField
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public class GroupingCollection2 extends HierarchicalData implements IGroupingCollection2
{
include "../core/Version.as";
//--------------------------------------------------------------------------
//
// Constructor
//
//--------------------------------------------------------------------------
/**
* Constructor.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 3
*/
public function GroupingCollection2()
{
super();
newCollection = new ArrayCollection();
super.source = newCollection;
objectSummaryMap = new Dictionary(false);
parentMap = {};
}
//--------------------------------------------------------------------------
//
// Variables
//
//--------------------------------------------------------------------------
/**
* @private
*
* denotes if the refresh is asynchronous.
*/
private var async:Boolean = false;
/**
* @private
*
* if true, dispatch collection change events
*/
private var dispatchCollectionEvents:Boolean = false;
/**
* @private
*/
private var newCollection:ArrayCollection;
/**
* @private
*/
private var _sourceCol:ICollectionView;
/**
* @private
*
* the object summary map.
* keeps summaries corresponding to different objects
*/
private var objectSummaryMap:Dictionary;
/**
* @private
*
* the original sort applied to the source collection
*/
private var oldSort:Sort;
/**
* @private
*/
private var prepared:Boolean = false;
/**
* @private
*/
private var flatView:ICollectionView;
/**
* @private
*/
private var flatCursor:IViewCursor;
/**
* @private
*/
private var hView:ICollectionView;
/**
* @private
*/
private var currentPosition:CursorBookmark = CursorBookmark.FIRST;
/**
* @private
*/
private var gf:Array;
/**
* @private
*/
private var fieldCount:int;
/**
* @private
*
* contains current data being compared
*/
private var currentData:Object;
/**
* @private
*
* contains current group objects for different group fields
*/
private var currentGroups:Array;
/**
* @private
*
* contains current group labels for different group fields
*/
private var currentGroupLabels:Array;
/**
* @private
*
* contains next index for different group fields
*/
private var currentIndices:Array;
/**
* @private
*
* item index
*/
private var itemIndex:int;
/**
* @private
*
* the children array for the group
*/
private var childrenArray:Array;
/**
* @private
*/
private var summaryPresent:Boolean;
/**
* The timer which is associated with an asynchronous refresh operation.
* You can use it to change the timing interval, pause the refresh,
* or perform other actions.
*
* The default value for the <code>delay</code> property of the
* Timer instance is 1, corresponding to 1 millisecond.
*
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 3
*/
protected var timer:Timer;
/**
* @private
* Mapping of UID to parents. Must be maintained as things get removed/added
* This map is created as objects are visited
*/
protected var parentMap:Object;
/**
* @private
* A Dictionary to maintain summaries
*/
private var summariesTracker:Dictionary;
/**
* @private
* An array to store all the summary fields.
*/
private var summaryFields:Array;
//--------------------------------------------------------------------------
//
// Properties
//
//--------------------------------------------------------------------------
//----------------------------------
// grouping
//----------------------------------
private var _grouping:Grouping;
/**
* Specifies the Grouping instance applied to the source data.
* Setting the <code>grouping</code> property
* does not automatically refresh the view,
* so you must call the <code>refresh()</code> method
* after setting this property.
*
* @see mx.collections.GroupingCollection2#refresh()
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 3
*/
public function get grouping():Grouping
{
return _grouping;
}
/**
* @private
*/
public function set grouping(value:Grouping):void
{
_grouping = value;
}
//----------------------------------
// summaries
//----------------------------------
/**
* Array of SummaryRow instances that define any root-level data summaries.
* Specify one or more SummaryRow instances to define the data summaries,
* as the following example shows:
*
* <pre>
* &lt;mx:AdvancedDataGrid id="myADG"
* width="100%" height="100%"
* initialize="gc.refresh();"&gt;
* &lt;mx:dataProvider&gt;
* &lt;mx:GroupingCollection2 id="gc" source="{dpFlat}"&gt;
* &lt;mx:summaries&gt;
* &lt;mx:SummaryRow summaryPlacement="last"&gt;
* &lt;mx:fields&gt;
* &lt;mx:SummaryField2 dataField="Actual"
* label="Min Actual" summaryOperation="MIN"/&gt;
* &lt;mx:SummaryField2 dataField="Actual"
* label="Max Actual" summaryOperation="MAX"/&gt;
* &lt;/mx:fields&gt;
* &lt;/mx:SummaryRow&gt;
* &lt;/mx:summaries&gt;
* &lt;mx:Grouping&gt;
* &lt;mx:GroupingField name="Region"/&gt;
* &lt;mx:GroupingField name="Territory"/&gt;
* &lt;/mx:Grouping&gt;
* &lt;/mx:GroupingCollection2&gt;
* &lt;/mx:dataProvider&gt;
*
* &lt;mx:columns&gt;
* &lt;mx:AdvancedDataGridColumn dataField="Region"/&gt;
* &lt;mx:AdvancedDataGridColumn dataField="Territory_Rep"
* headerText="Territory Rep"/&gt;
* &lt;mx:AdvancedDataGridColumn dataField="Actual"/&gt;
* &lt;mx:AdvancedDataGridColumn dataField="Estimate"/&gt;
* &lt;mx:AdvancedDataGridColumn dataField="Min Actual"/&gt;
* &lt;mx:AdvancedDataGridColumn dataField="Max Actual"/&gt;
* &lt;/mx:columns&gt;
* &lt;/mx:AdvancedDataGrid&gt;</pre>
*
* @see mx.collections.SummaryRow
* @see mx.collections.SummaryField2
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 3
*/
public var summaries:Array;
//--------------------------------------------------------------------------
//
// Overriden Methods
//
//--------------------------------------------------------------------------
/**
* The source collection containing the flat data to be grouped.
*
* If the source is not a collection, it will be auto-wrapped into a collection.
*
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 3
*/
override public function get source():Object
{
return _sourceCol;
}
override public function set source(value:Object):void
{
if (_sourceCol)
_sourceCol.removeEventListener(CollectionEvent.COLLECTION_CHANGE, collectionChangeHandler);
if (!value)
{
_sourceCol = null;
return;
}
_sourceCol = getCollection(value);
if(_sourceCol is ICollectionView)
_sourceCol.addEventListener(CollectionEvent.COLLECTION_CHANGE, collectionChangeHandler);
}
/**
* @private
*/
override public function getChildren(node:Object):Object
{
var children:Object = super.getChildren(node);
// populate the parentMap
// parentMap will be populated only if the children is ICollectionView
var uid:String;
if (children != null)
{
if (children is ICollectionView)
{
var cursor:IViewCursor = ICollectionView(children).createCursor();
while (!cursor.afterLast)
{
uid = UIDUtil.getUID(cursor.current);
parentMap[uid] = node;
cursor.moveNext();
}
}
else
{
//if the children is not ICollectionView then
//it was not introduced by GC. (this happens in the case of XML.)
return null;
}
}
return children;
}
/**
* Return <code>super.source</code>, if the <code>grouping</code> property is set,
* and an ICollectionView instance that refers to <code>super.source</code> if not.
*
* @return The object to return.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 3
*/
override public function getRoot():Object
{
return super.source;
}
//--------------------------------------------------------------------------
//
// Methods
//
//--------------------------------------------------------------------------
/**
* @inheritDoc
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 3
*/
public function refresh(async:Boolean = false, dispatchCollectionEvents:Boolean = false):Boolean
{
this.async = async;
this.dispatchCollectionEvents = async ? true : dispatchCollectionEvents;
var resetEvent:CollectionEvent;
// return if no grouping or groupingFields are supplied
if (!grouping || grouping.fields.length < 1 )
{
super.source = source;
// dispatch collection change event of kind reset.
resetEvent =
new CollectionEvent(CollectionEvent.COLLECTION_CHANGE);
resetEvent.kind = CollectionEventKind.RESET;
dispatchEvent(resetEvent);
return true;
}
super.source = newCollection;
// remove the items from the source collection if there are any.
newCollection.removeAll();
// dispatch collection change event of kind reset.
resetEvent =
new CollectionEvent(CollectionEvent.COLLECTION_CHANGE);
resetEvent.kind = CollectionEventKind.RESET;
dispatchEvent(resetEvent);
// reset the parent map
parentMap = {};
// reset the object summary map
objectSummaryMap = new Dictionary(false);
// reset the summaryTracker
summariesTracker = null;
// check if any summary is specified
summaryPresent = false;
prepareSummaryFields();
var grouped:Boolean;
if(source && grouping)
{
grouped = makeGroupedCollection();
}
return grouped;
}
/**
* @inheritDoc
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 3
*/
public function cancelRefresh():void
{
if (timer)
{
timer.stop();
timer = null;
cleanUp();
}
}
/**
* @private
*
* check for the existing groups if they can accomodate the item
* otherwise create new group and place the item.
*/
private function addItem(node:Object, depth:int = 0):void
{
var coll:ICollectionView = super.source as ICollectionView;
var parent:Object = null;
var i:int = 0;
while (i < grouping.fields.length)
{
// check for the parent item
var obj:Object = checkForParentExistence(coll, node, i);
// if no parent, then use the previous parent
if (!obj)
break;
// change the parent to point to its parent and start looking for parent again
parent = obj;
if (parent)
{
coll = getChildren(parent) as ICollectionView;
i++;
}
}
// parent found
if (parent)
{
coll = getChildren(parent) as ICollectionView;
// create groups for the item as no existing group matched with the item
if (i <= grouping.fields.length-1)
{
// create groups and place item
createGroupsAndInsertItem(coll, node, i);
// update the summary for the parents
updateSummary(node);
return;
}
// if there already exist a group for the item and insert the item in that group
if (coll is IList)
{
// insert the item
IList(coll).addItem(node);
// update the parent map
updateParentMap(parent, node);
// update the summary for the parents
updateSummary(node);
}
}
else
{
// no groups for the item, create groups and add item to them
createGroupsAndInsertItem(super.source as ICollectionView, node);
// update the summary for the parents
updateSummary(node);
}
}
/**
* @private
*
* check for existence of the groups for an item in the collection
* and return all the parent of that item.
*/
private function checkForParentExistence(coll:ICollectionView, node:Object, depth:int):Object
{
var cursor:IViewCursor = coll.createCursor();
var label:String = getDataLabel(node, grouping.fields[depth]);
while (!cursor.afterLast)
{
var current:Object = cursor.current;
// check for matching fields
if (current.hasOwnProperty(grouping.label) &&
current[grouping.label] == label)
{
return current;
}
cursor.moveNext();
}
return null;
}
/**
* @private
*
* creating the group for the item in the collection
* and placing it there.
*/
private function createGroupsAndInsertItem(coll:ICollectionView, node:Object, depth:int = 0):void
{
var parent:Object;
var n:int = grouping.fields.length;
for (var i:int = depth; i < n ; i++)
{
// get the dataField and value
var dataField:String = grouping.fields[i].name;
var value:String = getDataLabel(node, grouping.fields[i]);
if (!value)
return;
// create a new group
var obj:Object;
if (grouping.fields[i].groupingObjectFunction != null)
obj = grouping.fields[i].groupingObjectFunction(value);
else if (grouping.groupingObjectFunction != null)
obj = grouping.groupingObjectFunction(value);
else
obj = {};
obj[childrenField] = new ArrayCollection();
obj[grouping.label] = value;
if (coll is IList)
{
IList(coll).addItem(obj);
// get the parent and update parent map
parent = parent != null ? parent : getParent(coll.createCursor().current);
updateParentMap(parent, obj);
coll = getChildren(obj) as ICollectionView;
parent = obj;
// if we reach the end of the fields, just insert the item in the collection
if (i == n - 1)
{
IList(coll).addItem(node);
// update the parent map
updateParentMap(obj, node);
break;
}
}
}
}
/**
* @private
*
* returns the collection view of the given object
*/
private function getCollection(value:Object):ICollectionView
{
// handle strings and xml
if (typeof(value)=="string")
value = new XML(value);
else if (value is XMLNode)
value = new XML(XMLNode(value).toString());
else if (value is XMLList)
value = new XMLListCollection(value as XMLList);
if (value is XML)
{
var xl:XMLList = new XMLList();
xl += value;
return new XMLListCollection(xl);
}
//if already a collection dont make new one
else if (value is ICollectionView)
{
return ICollectionView(value);
}
else if (value is Array)
{
return new ArrayCollection(value as Array);
}
//all other types get wrapped in an ArrayCollection
else if (value is Object)
{
// convert to an array containing this one item
var tmp:Array = [];
tmp.push(value);
return new ArrayCollection(tmp);
}
else
{
return new ArrayCollection();
}
}
/**
* @private
* get the data label from user specified function.
* otherwise get the label from the data.
*/
private function getDataLabel(data:Object,field:GroupingField):String
{
if (field.groupingFunction != null)
return field.groupingFunction(data, field);
// should we create a defaultGroupLabelValue property
return data.hasOwnProperty(field.name) ? data[field.name] : "Not Available";
}
/**
* Returns the parent of a node.
* The parent of a top-level node is <code>null</code>.
*
* @param node The Object that defines the node.
*
* @return The parent node containing the node as child,
* <code>null</code> for a top-level node,
* and <code>undefined</code> if the parent cannot be determined.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 3
*/
protected function getParent(node:Object):*
{
var uid:String = UIDUtil.getUID(node);
if (parentMap.hasOwnProperty(uid))
return parentMap[uid];
return undefined;
}
/**
* @private
* Generate the root level summaries
*/
private function generateRootSummaries(flatData:Boolean = false):void
{
var coll:ICollectionView = super.source as ICollectionView;
getSummaries(coll, -1);
}
/**
* @private
* Calculate summaries for a node.
*/
private function getSummariesForRow(node:Object, collection:ICollectionView, depth:int):void
{
// calculate the summary at the last level
var hd:HierarchicalData = new HierarchicalData(collection);
hd.childrenField = this.childrenField;
var hColl:ICollectionView = new HierarchicalCollectionView(hd, {});
var hCursor:IViewCursor = new LeafNodeCursor(HierarchicalCollectionView(hColl), hColl, hd);
// for all summary fields
var m:int = summaryFields.length;
var i:int = 0;
var defaultSummaryCalculator:DefaultSummaryCalculator = new DefaultSummaryCalculator();
var summaryMap:Dictionary = new Dictionary(false);
var summaryField:SummaryField2;
var summaryCalculator:ISummaryCalculator;
for (i = 0; i < m; i++)
{
summaryField = summaryFields[i];
if (!(summaryField.summaryOperation is String) &&
summaryField.summaryOperation is ISummaryCalculator)
summaryCalculator = ISummaryCalculator(summaryField.summaryOperation);
else
summaryCalculator = defaultSummaryCalculator;
summaryMap[summaryField] = summaryCalculator.summaryCalculationBegin(summaryField);
}
while (!hCursor.afterLast)
{
for (i = 0; i < m; i++)
{
summaryField = summaryFields[i];
if (!(summaryField.summaryOperation is String) &&
summaryField.summaryOperation is ISummaryCalculator)
summaryCalculator = ISummaryCalculator(summaryField.summaryOperation);
else
summaryCalculator = defaultSummaryCalculator;
summaryCalculator.calculateSummary(summaryMap[summaryField], summaryField, hCursor.current);
}
hCursor.moveNext();
}
for (i = 0; i < m; i++)
{
var summary:Number = 0.0;
summaryField = summaryFields[i];
if (!(summaryField.summaryOperation is String) &&
summaryField.summaryOperation is ISummaryCalculator)
summaryCalculator = ISummaryCalculator(summaryField.summaryOperation);
else
summaryCalculator = defaultSummaryCalculator;
summary = summaryCalculator.returnSummary(summaryMap[summaryField], summaryField);
populateSummary(node, summaryField, summaryMap[summaryField], summary);
}
}
/**
* @private
* Store the summary in a dictionary for later use.
*/
private function populateSummary(node:Object, summaryField:SummaryField2, summaryObject:Object, summary:Number):void
{
var op:String = summaryField.summaryOperation.toString();
if (summariesTracker == null)
summariesTracker = new Dictionary(false);
if (summariesTracker[node] == undefined)
summariesTracker[node] = new Dictionary(false);
if (summariesTracker[node][op] == undefined)
summariesTracker[node][op] = new Dictionary(false);
if (summariesTracker[node][op][summaryField.dataField] == undefined)
summariesTracker[node][op][summaryField.dataField] = new Dictionary(false);
summariesTracker[node][op][summaryField.dataField] = {summaryObject:summaryObject, value:summary};
}
/**
* @private
* Get the summary from the dictionary
*/
private function getSummary(node:Object, summaryField:SummaryField2):Object
{
var op:String = summaryField.summaryOperation.toString();
if (summariesTracker == null ||
summariesTracker[node] == undefined ||
summariesTracker[node][op] == undefined ||
summariesTracker[node][op][summaryField.dataField] == undefined)
return null;
return summariesTracker[node][op][summaryField.dataField];
}
/**
* @private
* Calcualte and insert summary for a node
*/
private function getSummaries(node:Object, depth:int):void
{
if (depth > grouping.fields.length - 1)
return;
var children:ICollectionView = this.getChildren(node) as ArrayCollection;
if (node == super.source)
children = node as ICollectionView;
if (!children || children.length == 0)
return;
if (depth == grouping.fields.length - 1)
{
// calculate actual summaries here
getSummariesForRow(node, children, depth);
}
else
{
var cursor:IViewCursor = children.createCursor();
var isFirst:Boolean = true;
var defaultSummaryCalculator:DefaultSummaryCalculator = new DefaultSummaryCalculator();
var summaryField:SummaryField2;
var summaryMap:Dictionary = new Dictionary(false);
var summaryCalculator:ISummaryCalculator;
while (!cursor.afterLast)
{
var current:Object = cursor.current;
if (!(current is SummaryObject))
{
// for all summary fields
var m:int = summaryFields.length;
var i:int = 0;
for (i = 0; i < m; i++)
{
summaryField = summaryFields[i];
if (!(summaryField.summaryOperation is String) &&
summaryField.summaryOperation is ISummaryCalculator)
summaryCalculator = ISummaryCalculator(summaryField.summaryOperation);
else
summaryCalculator = defaultSummaryCalculator;
var summaryObject:Object = getSummary(current, summaryField);
if (isFirst)
{
summaryMap[summaryField] = summaryCalculator.summaryOfSummaryCalculationBegin(summaryObject["summaryObject"], summaryField);
}
else
{
summaryCalculator.calculateSummaryOfSummary(summaryMap[summaryField], summaryObject["summaryObject"], summaryField);
}
}
}
isFirst = false;
cursor.moveNext();
}
// check if there were some items
if (!isFirst)
{
for (i = 0; i < m; i++)
{
summaryField = summaryFields[i];
if (!(summaryField.summaryOperation is String) &&
summaryField.summaryOperation is ISummaryCalculator)
summaryCalculator = ISummaryCalculator(summaryField.summaryOperation);
else
summaryCalculator = defaultSummaryCalculator;
var summary:Number = summaryCalculator.returnSummaryOfSummary(summaryMap[summaryField], summaryField);
populateSummary(node, summaryField, summaryMap[summaryField], summary);
}
}
}
if (depth == -1)
insertSummaries(super.source as ICollectionView, -1, true);
else
insertSummaries(node, depth);
}
private function insertSummaries(node:Object, depth:int, rootSummary:Boolean = false):void
{
var summaries:Array = this.summaries;
if (!rootSummary)
summaries = grouping.fields[depth].summaries;
if (!summaries)
return;
var summaryObj:Array = [];
var n:int = summaries.length;
for (var i:int = 0; i < n; i++)
{
var summaryRow:SummaryRow = summaries[i];
summaryObj[i] =
summaryRow.summaryObjectFunction != null ?
summaryRow.summaryObjectFunction() : new SummaryObject();
// for all summary fields
var m:int = summaryRow.fields.length;
for (var j:int = 0; j < m; j++)
{
var summaryField:SummaryField2 = summaryRow.fields[j];
var summary:Number = 0.0;
var label:String = summaryField.label ? summaryField.label : summaryField.dataField;
var summaryObject:Object = getSummary(node, summaryField);
if (summaryObject != null)
{
summary = summaryObject["value"];
// populate the summary object
summaryObj[i][label] = summary;
}
}
// populate the object summary map
if (objectSummaryMap[node] == undefined)
objectSummaryMap[node] = [];
objectSummaryMap[node].push(summaryObj[i]);
}
// insert the summary
if (rootSummary)
insertRootSummary(summaryObj);
else
insertSummary(node, summaryObj, summaries);
}
/**
* @private
*
* initialize the variables
*
*/
private function initialize():void
{
currentData = null;
currentGroups = [];
currentGroupLabels = [];
currentIndices = [] ;
childrenArray = null;
}
/**
* @private
*
* insert the root summaries
*
*/
private function insertRootSummary(summaryObj:Array):void
{
var coll:ICollectionView = super.source as ICollectionView;
if (!(coll is IList))
return;
var n:int = summaryObj.length;
for (var i:int = 0; i < n; i++)
{
var summaryRow:SummaryRow = summaries[i];
if (summaryRow.summaryPlacement.indexOf("first") != -1)
{
IList(coll).addItemAt(summaryObj[i], 0);
}
if (summaryRow.summaryPlacement.indexOf("last") != -1)
{
IList(coll).addItem(summaryObj[i]);
}
}
}
/**
* @private
*
* inserts the summaries in the children collection of the parent node
*/
private function insertSummary(parent:Object, summaryObj:Array, summaries:Array):void
{
var n:int = summaries.length;
for (var i:int = 0; i < n; i++)
{
var summaryRow:SummaryRow = summaries[i];
if (summaryRow.summaryPlacement.indexOf("group") != -1)
{
for (var p:String in summaryObj[i])
parent[p] = summaryObj[i][p];
}
var children:IList;
if (summaryRow.summaryPlacement.indexOf("first") != -1)
{
children = (getChildren(parent) as ArrayCollection);
if (children)
children.addItemAt(summaryObj[i], 0);
}
if (summaryRow.summaryPlacement.indexOf("last") != -1)
{
children = (getChildren(parent) as ArrayCollection);
if (children)
children.addItem(summaryObj[i]);
}
}
}
/**
* @private
*/
private function makeGroupedCollection():Boolean
{
// save the sorting information of the source collection
// sort the source collection and create a grouped collection
// restore the sort of the original source collection
var fields:Array = [];
var n:int = grouping.fields.length;
for (var i:int = 0; i < n; i++)
{
var groupingField:GroupingField = grouping.fields[i];
var sortField:SortField = new SortField(groupingField.name,
groupingField.caseInsensitive,
groupingField.descending, groupingField.numeric);
sortField.compareFunction = groupingField.compareFunction;
fields.push(sortField);
}
oldSort = source.sort;
source.sort = new Sort();
// Set the compare function
if (grouping.compareFunction != null)
source.sort.compareFunction = grouping.compareFunction;
source.sort.fields = fields;
var refreshed:Boolean = source.refresh();
if (!refreshed)
return refreshed;
if (async)
{
timer = new Timer(1);
timer.addEventListener(TimerEvent.TIMER, timerHandler);
timer.start();
}
else
{
return buildGroups();
}
return true;
}
/**
* @private
*/
private function timerHandler(event:TimerEvent):void
{
if (buildGroups())
{
timer.stop();
timer = null;
}
}
/**
* @private
*
* Start building the groups
*/
private function buildGroups():Boolean
{
if (!prepared)
{
var _openItems:Object = {};
// initialize the variables
initialize();
if ((source as ICollectionView).length == 0)
return false;
if (dispatchCollectionEvents)
{
var hierarchicalData:IHierarchicalData = new HierarchicalData(newCollection);
HierarchicalData(hierarchicalData).childrenField = childrenField;
hView = new HierarchicalCollectionView(
hierarchicalData, _openItems);
}
flatView = source as ICollectionView;
flatCursor = flatView.createCursor();
gf = grouping.fields;
fieldCount = gf.length;
if (gf)
{
prepared = true;
if (async)
return false;
}
if (async)
return true;
}
flatCursor.seek(currentPosition);
while(!flatCursor.afterLast && currentPosition != CursorBookmark.LAST)
{
currentData = flatCursor.current;
var n:int = 0;
for (var i:int = 0; i < fieldCount ; ++i)
{
var groupingField:String = gf[i].name;
var label:String = getDataLabel(currentData, gf[i]);
if(label != currentGroupLabels[i])
{
if (childrenArray && childrenArray.length)
{
ArrayCollection(currentGroups[fieldCount - 1][childrenField]).source = childrenArray;
childrenArray = [];
}
// calculate summaries for created groups
if (summaryPresent)
{
for (n = currentGroups.length - 1; n >= i; n--)
getSummaries(currentGroups[n], n);
}
currentGroupLabels.splice(i+1);
currentGroups.splice(i+1);
currentIndices.splice(i+1);
currentGroupLabels[i] = label;
// check for grouping Object Function
if (gf[i].groupingObjectFunction != null)
currentGroups[i] = gf[i].groupingObjectFunction(label);
else if (grouping.groupingObjectFunction != null)
currentGroups[i] = grouping.groupingObjectFunction(label);
else
currentGroups[i] = {};
currentGroups[i][childrenField] = new ArrayCollection();
currentGroups[i][grouping.label] = currentGroupLabels[i];
itemIndex = currentIndices[i-1];
// create the group
if (dispatchCollectionEvents)
{
IHierarchicalCollectionView(hView).addChild(currentGroups[i-1], currentGroups[i]);
} else
{
if (i > 0)
{
currentGroups[i-1][childrenField].source.push(currentGroups[i]);
}
else
{
newCollection.source.push(currentGroups[i]);
}
}
currentIndices[i-1] = ++itemIndex;
}
// insert the node as a child of the group
if ( i == fieldCount - 1)
{
itemIndex = currentIndices[i];
if (!childrenArray)
childrenArray = [];
childrenArray.push(currentData);
currentIndices[i] = ++itemIndex;
}
}
try
{
flatCursor.moveNext();
currentPosition = flatCursor.bookmark;
// return in case of async refresh
if (async)
return false;
}
catch (e:ItemPendingError)
{
cleanUp();
e.addResponder(new ItemResponder(
function(data:Object, token:Object=null):void
{
makeGroupedCollection();
},
function(info:Object, token:Object=null):void
{
//no-op
}));
}
}
if (currentPosition == CursorBookmark.LAST)
{
if (childrenArray && childrenArray.length)
{
ArrayCollection(currentGroups[fieldCount - 1][childrenField]).source = childrenArray;
// calculate summaries for created groups
if (summaryPresent)
{
for (n = fieldCount - 1; n >= 0; n--)
getSummaries(currentGroups[n], n);
}
}
// calculate root summaries
if (source && summaries)
{
if (!super.source)
super.source = new ArrayCollection([source]) as Object;
generateRootSummaries(grouping == null);
}
// refresh the collection to reflect the changes
// we made while grouping.
// This is needed as we made changes directly to
// the source of the collection when
// dispatchCollectionEvents is false
newCollection.refresh();
// dispatch collection change event of kind refresh.
var refreshEvent:CollectionEvent =
new CollectionEvent(CollectionEvent.COLLECTION_CHANGE);
refreshEvent.kind = CollectionEventKind.REFRESH;
dispatchEvent(refreshEvent);
cleanUp();
}
return true;
}
/**
* @private
* Get all the summary fields and store them in an Array
*/
private function prepareSummaryFields():void
{
summaryFields = [];
var sr:SummaryRow
// for grouping fields summaries
if (grouping.fields != null)
{
for (var i:int = 0; i < grouping.fields.length; i++)
{
var gf:GroupingField = grouping.fields[i];
if (gf.summaries != null)
{
for (var j:int = 0; j < gf.summaries.length; j++)
{
sr = gf.summaries[j];
if (sr.fields != null)
{
for (var k:int = 0; k < sr.fields.length; k++)
{
summaryPresent = true;
summaryFields.push(sr.fields[k]);
}
}
}
}
}
}
// for root level summaries
if (summaries != null)
{
for (i = 0; i < summaries.length; i++)
{
sr = summaries[i];
if (sr.fields != null)
{
for (j = 0; j < sr.fields.length; j++)
{
summaryPresent = true;
summaryFields.push(sr.fields[j]);
}
}
}
}
}
/**
* @private
*
* Restores the original sort in the source collection
* and clean up all the variables.
*/
private function cleanUp():void
{
source.sort = oldSort;
source.refresh();
prepared = false;
currentPosition = CursorBookmark.FIRST;
oldSort = null;
flatCursor = null;
}
/**
* @private
*
* It will call the function func on each node of the collection.
*/
private function applyFunctionForParentNodes(coll:ICollectionView, func:Function, depth:int = 0):void
{
if (coll.length > 0)
{
var masterCursor:IViewCursor = coll.createCursor();
while(!masterCursor.afterLast)
{
var child:Object = getChildren(masterCursor.current);
if (child is ArrayCollection)
{
var childCursor:IViewCursor = ArrayCollection(child).createCursor();
while (!childCursor.afterLast)
{
var current:Object = childCursor.current;
// recurse over each child
if (this.hasChildren(current))
{
applyFunctionForParentNodes(child as ArrayCollection, func, depth + 1);
break;
}
childCursor.moveNext();
}
}
// calculate for the parent node
func(masterCursor.current, depth);
masterCursor.moveNext();
}
}
}
/**
* @private
*
* removes the root summaries and
* generates them again.
*
*/
private function regenerateRootSummaries():void
{
if(!summaries)
return;
var coll:ICollectionView = super.source as ICollectionView;
if (!grouping)
coll = coll.createCursor().current as ICollectionView;
var summaryObj:Array = objectSummaryMap[coll];
// for the first time there will be no summaries
// so, this check will fail and root summaries wont
// be removed and regenerated again.
// for all the other times, the root summaries will be
// regenerated.
if (!summaryObj || !(coll is IList))
return;
// delete all the root level summaries
var n:int = summaryObj.length;
for (var i:int = 0; i < n; i++)
{
var index:int = IList(coll).getItemIndex(summaryObj[i]);
if (index != -1)
IList(coll).removeItemAt(index);
}
delete objectSummaryMap[coll];
// generate the root level summaries
generateRootSummaries(grouping == null);
}
/**
* @private
*
* removes all the summaries from the collection
*/
private function removeAllSummaries():void
{
// call generate summary with remove summary function as parameter
applyFunctionForParentNodes(super.source as ICollectionView, removeSummary);
}
/**
* @private
*
* remove the summaries for an item and its parents.
* return the parents of the node going one level up each time.
*
*/
private function removeItemAndSummaries(coll:ICollectionView, node:Object, removeItem:Boolean = false):Array
{
var parentNodes:Array = [];
var parent:Object = getParent(node);
while (parent != null)
{
var index:int;
var addParent:Boolean = true;
var children:ICollectionView = getChildren(parent) as ArrayCollection;
if (children)
{
if (children.contains(node))
{
if (children is IList)
{
if (removeItem)
{
// remove the item from the group
index = IList(children).getItemIndex(node);
if (index != -1)
{
IList(children).removeItemAt(index);
// delete item from parent map
var uid:String = UIDUtil.getUID(node);
if (parentMap[uid])
delete parentMap[uid];
}
}
if (objectSummaryMap[parent])
{
var temp:Array = objectSummaryMap[parent];
var n:int = temp.length;
for (var i:int = 0; i < n; i++)
{
index = IList(children).getItemIndex(temp[i]);
if (index != -1)
{
// remove the summary information for the group
IList(children).removeItemAt(index);
}
}
// delete summary from parent summary map
delete objectSummaryMap[parent];
}
if (summariesTracker != null && summariesTracker[parent])
summariesTracker[parent] = new Dictionary(false);
if (removeItem)
{
// remove the group if no children is present
if (children.length == 0 && getParent(parent) != null)
{
addParent = false;
}
else
removeItem = false;
}
}
}
if (addParent)
parentNodes.push(parent);
node = parent;
parent = getParent(parent);
if (!parent)
{
// remove the parent node from the collection if its the only one remaining.
// first check for the child collection length
if (parentNodes.length == 1)
{
if ((getChildren(parentNodes[0]) as ICollectionView).length == 0 && coll is IList)
{
index = IList(coll).getItemIndex(node);
if (index != -1)
{
IList(coll).removeItemAt(index);
return null;
}
}
}
return parentNodes.reverse();
}
}
}
return null;
}
/**
* @private
*
* removes the summaries from the parent node and all its children.
*
*/
private function removeSummary(parent:Object, depth:int):void
{
var children:ICollectionView = this.getChildren(parent) as ArrayCollection;
if (!children)
return;
// remove summaries from the parents of the current object
removeItemAndSummaries(super.source as ICollectionView, children.createCursor().current);
}
/**
* @private
*
* updates the parent map
*/
private function updateParentMap(parent:Object, node:Object):void
{
var uid:String = UIDUtil.getUID(node);
parentMap[uid] = parent;
}
/**
* @private
*
* removes the summary information and regenrates it for a particular node
* and its parents.
* optionally removes the item from the group also.
*/
private function updateSummary(node:Object, removeItem:Boolean = false):void
{
var coll:ICollectionView = super.source as ICollectionView;
var parentNodes:Array;
if (summaryPresent || removeItem)
parentNodes = removeItemAndSummaries(coll, node, removeItem);
if (summaryPresent && parentNodes)
{
var n:int = parentNodes.length;
for (var i:int = n - 1; i >= 0; i--)
{
getSummaries(parentNodes[i], i);
}
}
}
//--------------------------------------------------------------------------
//
// Event handlers
//
//--------------------------------------------------------------------------
/**
* @private
*
* collection change handler.
*
*/
private function collectionChangeHandler(event:CollectionEvent):void
{
if (!grouping)
return;
var i:int = 0;
var j:int = 0;
var n:int = 0;
var m:int = 0;
var obj:Object;
if (event.kind == CollectionEventKind.UPDATE)
{
n = event.items.length;
for (i = 0; i < n; i++)
{
var summaryCalculated:Boolean;
// take the source property to get the updated object
obj = event.items[i].source;
if (!obj)
continue;
m = grouping.fields.length;
for (j = 0; j < m; j++)
{
// check if the property corresponding to the group fields changed
if (event.items[i].property == grouping.fields[j].name)
{
summaryCalculated = true;
// update summaries - first remove the item from its group
updateSummary(obj,true);
// add the item to the group
addItem(obj);
break;
}
}
// for other properties just update the summaries
if (!summaryCalculated)
updateSummary(obj);
}
}
if (event.kind == CollectionEventKind.ADD)
{
n = event.items.length;
for (i = 0; i < n; i++)
{
obj = event.items[i];
// add the item to the group
addItem(obj);
}
}
if (event.kind == CollectionEventKind.REMOVE)
{
n = event.items.length;
for (i = 0; i < n; i++)
{
obj = event.items[i];
// update summaries - first remove the item from its group
updateSummary(obj,true);
}
}
if (event.kind == CollectionEventKind.REPLACE)
{
n = event.items.length;
for (i = 0; i < n; i++)
{
var oldValue:Object = event.items[i].oldValue;
var newValue:Object = event.items[i].newValue;
// update summaries - first remove the item from its group
updateSummary(oldValue,true);
// add the item to the group
addItem(newValue);
}
}
// generate the root summaries
regenerateRootSummaries();
}
}
}