| //////////////////////////////////////////////////////////////////////////////// |
| // |
| // 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> |
| * <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> |
| * </pre> |
| * |
| * @mxml |
| * |
| * The <code><mx.GroupingCollection2></code> inherits all the tag attributes of its superclass, |
| * and defines the following tag attributes:</p> |
| * |
| * <pre> |
| * <mx:GroupingCollection2 |
| * <b>Properties </b> |
| * grouping="<i>No default</i>" |
| * source="<i>No default</i>" |
| * summaries="<i>No default</i>" |
| * /> |
| * </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> |
| * <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></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(); |
| } |
| } |
| |
| } |