//////////////////////////////////////////////////////////////////////////////// // // 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. * *

Note: 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.

* *

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 GroupingCollection2.grouping property. * The Grouping instance contains an Array of GroupingField instances, * one per grouping field.

* *

The following example uses the GroupingCollection2 class to define * two grouping fields: Region and Territory.

* *
 *  <mx:AdvancedDataGrid id="myADG"    
 *    <mx:dataProvider> 
 *      <mx:GroupingCollection2 id="gc" source="{dpFlat}"> 
 *        <mx:grouping> 
 *          <mx:Grouping> 
 *            <mx:GroupingField name="Region"/> 
 *            <mx:GroupingField name="Territory"/> 
 *          </mx:Grouping> 
 *        </mx:grouping> 
 *      </mx:GroupingCollection2> 
 *    </mx:dataProvider>  
 *     
 *    <mx:columns> 
 *      <mx:AdvancedDataGridColumn dataField="Region"/> 
 *      <mx:AdvancedDataGridColumn dataField="Territory"/> 
 *      <mx:AdvancedDataGridColumn dataField="Territory_Rep"/> 
 *      <mx:AdvancedDataGridColumn dataField="Actual"/> 
 *      <mx:AdvancedDataGridColumn dataField="Estimate"/> 
 *    </mx:columns> 
 *  </mx:AdvancedDataGrid>
 *  
* * @mxml * * The <mx.GroupingCollection2> inherits all the tag attributes of its superclass, * and defines the following tag attributes:

* *
 *  <mx:GroupingCollection2
 *  Properties 
 *    grouping="No default"
 *    source="No default"
 *    summaries="No default"
 *  />
 *  
* * @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 delay 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 grouping property * does not automatically refresh the view, * so you must call the refresh() 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: * *
     *  <mx:AdvancedDataGrid id="myADG" 
     *     width="100%" height="100%" 
     *     initialize="gc.refresh();">        
     *     <mx:dataProvider>
     *         <mx:GroupingCollection2 id="gc" source="{dpFlat}">
     *             <mx:summaries>
     *                 <mx:SummaryRow summaryPlacement="last">
     *                     <mx:fields>
     *                         <mx:SummaryField2 dataField="Actual" 
     *                             label="Min Actual" summaryOperation="MIN"/>
     *                         <mx:SummaryField2 dataField="Actual" 
     *                             label="Max Actual" summaryOperation="MAX"/>
     *                     </mx:fields>
     *                   </mx:SummaryRow>
     *                 </mx:summaries>
     *             <mx:Grouping>
     *                 <mx:GroupingField name="Region"/>
     *                 <mx:GroupingField name="Territory"/>
     *             </mx:Grouping>
     *         </mx:GroupingCollection2>
     *     </mx:dataProvider>        
     *     
     *     <mx:columns>
     *         <mx:AdvancedDataGridColumn dataField="Region"/>
     *         <mx:AdvancedDataGridColumn dataField="Territory_Rep"
     *             headerText="Territory Rep"/>
     *         <mx:AdvancedDataGridColumn dataField="Actual"/>
     *         <mx:AdvancedDataGridColumn dataField="Estimate"/>
     *         <mx:AdvancedDataGridColumn dataField="Min Actual"/>
     *         <mx:AdvancedDataGridColumn dataField="Max Actual"/>
     *     </mx:columns>
     *  </mx:AdvancedDataGrid>
* * @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 super.source, if the grouping property is set, * and an ICollectionView instance that refers to super.source 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 null. * * @param node The Object that defines the node. * * @return The parent node containing the node as child, * null for a top-level node, * and undefined 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(); } } }