001: /**
002: * Copyright (c) 2004-2006 Regents of the University of California.
003: * See "license-prefuse.txt" for licensing terms.
004: */package prefuse.visual;
005:
006: import java.util.HashSet;
007: import java.util.Iterator;
008:
009: import prefuse.Visualization;
010: import prefuse.data.Schema;
011: import prefuse.data.Table;
012: import prefuse.data.Tuple;
013: import prefuse.data.event.EventConstants;
014: import prefuse.data.util.Index;
015: import prefuse.util.collections.IntIterator;
016: import prefuse.visual.tuple.TableAggregateItem;
017:
018: /**
019: * VisualTable instance that maintains visual items representing aggregates
020: * of items. This class maintains both a collection of AggregateItems and
021: * a mapping between AggregateItems and the VisualItems contained within
022: * those aggregates.
023: *
024: * @author <a href="http://jheer.org">jeffrey heer</a>
025: */
026: public class AggregateTable extends VisualTable {
027:
028: /**
029: * Table storing the 1->Many aggregation mappings
030: */
031: protected Table m_aggregated;
032:
033: /**
034: * Create a new AggregateTable.
035: * @param vis the Visualization associated with the table
036: * @param group the data group the table contents belongs to
037: */
038: public AggregateTable(Visualization vis, String group) {
039: this (vis, group, VisualItem.SCHEMA);
040: }
041:
042: /**
043: * Create a new AggregateTable.
044: * @param vis the Visualization associated with the table
045: * @param group the data group the table contents belongs to
046: * @param schema the Schema to use for this table
047: */
048: public AggregateTable(Visualization vis, String group, Schema schema) {
049: super (vis, group, schema, TableAggregateItem.class);
050: m_aggregated = AGGREGATED_SCHEMA.instantiate();
051: m_aggregated.index(AGGREGATE);
052: m_aggregated.index(MEMBER_HASH);
053: }
054:
055: // ------------------------------------------------------------------------
056:
057: /**
058: * Get the size of the aggregate represented at the given table row.
059: * Returns the number of visual items contained in the aggregation.
060: * @return the aggregate size for the given row
061: */
062: public int getAggregateSize(int row) {
063: int size = 0;
064: AggregatedIterator ati = new AggregatedIterator(row);
065: for (; ati.hasNext(); ++size, ati.next())
066: ;
067: return size;
068: }
069:
070: /**
071: * Add an item to the aggregation at the given row.
072: * @param row the row index of the aggregate
073: * @param member the item to add to the aggregation
074: */
075: public void addToAggregate(int row, VisualItem member) {
076: validRowCheck(row, true);
077: if (!aggregateContains(row, member)) {
078: int ar = m_aggregated.addRow();
079: m_aggregated.setInt(ar, AGGREGATE, row);
080: m_aggregated.setInt(ar, MEMBER_HASH, getHashCode(member));
081: m_aggregated.set(ar, MEMBER, member);
082: fireTableEvent(row, row, EventConstants.ALL_COLUMNS,
083: EventConstants.UPDATE);
084: }
085: }
086:
087: /**
088: * Remove an item from the aggregation at the given row
089: * @param row the row index of the aggregate
090: * @param member the item to remove from the aggregation
091: */
092: public void removeFromAggregate(int row, VisualItem member) {
093: validRowCheck(row, true);
094: int ar = getAggregatedRow(row, member);
095: if (ar >= 0) {
096: m_aggregated.removeRow(ar);
097: fireTableEvent(row, row, EventConstants.ALL_COLUMNS,
098: EventConstants.UPDATE);
099: }
100: }
101:
102: /**
103: * Remove all items contained in the aggregate at the given row
104: * @param row the row index of the aggregate
105: */
106: public void removeAllFromAggregate(int row) {
107: clearAggregateMappings(row, true);
108: }
109:
110: /**
111: * Clears all aggregates mappings for the aggregate at the given row,
112: * optionally issuing a table update.
113: * @param row the table row of the aggregate
114: * @param update indicates whether or not to fire a table update
115: */
116: protected void clearAggregateMappings(int row, boolean update) {
117: Index index = m_aggregated.index(AGGREGATE);
118: boolean fire = false;
119: for (IntIterator rows = index.rows(row); rows.hasNext();) {
120: int r = rows.nextInt();
121: // this removal maneuver is ok because we know we are
122: // pulling row values directly from an index
123: // with intervening iterators, remove might throw an exception
124: rows.remove();
125: m_aggregated.removeRow(r);
126: fire = true;
127: }
128: if (update && fire)
129: fireTableEvent(row, row, EventConstants.ALL_COLUMNS,
130: EventConstants.UPDATE);
131: }
132:
133: /**
134: * Indicates if an item is a member of the aggregate at the given row
135: * @param row the table row of the aggregate
136: * @param member the item to check from containment
137: * @return true if the item is in the aggregate, false otherwise
138: */
139: public boolean aggregateContains(int row, VisualItem member) {
140: return getAggregatedRow(row, member) >= 0;
141: }
142:
143: /**
144: * Get the row index to the aggregate mapping table for the given
145: * aggregate and contained VisualItem.
146: * @param row the table row of the aggregate
147: * @param member the VisualItem to look up
148: * @return the row index into the internal aggregate mapping table for the
149: * mapping between the given aggregate row and given VisualItem
150: */
151: protected int getAggregatedRow(int row, VisualItem member) {
152: Index index = m_aggregated.index(MEMBER_HASH);
153: int hash = getHashCode(member);
154: int ar = index.get(hash);
155: if (ar < 0) {
156: return -1;
157: } else if (m_aggregated.getInt(ar, AGGREGATE) == row) {
158: return ar;
159: } else {
160: for (IntIterator rows = index.rows(hash); rows.hasNext();) {
161: ar = rows.nextInt();
162: if (m_aggregated.getInt(ar, AGGREGATE) == row)
163: return ar;
164: }
165: return -1;
166: }
167: }
168:
169: /**
170: * Get all VisualItems within the aggregate at the given table row.
171: * @param row the table row of the aggregate
172: * @return an iterator over the items in the aggregate
173: */
174: public Iterator aggregatedTuples(int row) {
175: return new AggregatedIterator(row);
176: }
177:
178: /**
179: * Get an iterator over all AggregateItems that contain the given Tuple.
180: * @param t the input tuple
181: * @return an iterator over all AggregateItems that contain the input Tuple
182: */
183: public Iterator getAggregates(Tuple t) {
184: int hash = getHashCode(t);
185: IntIterator iit = m_aggregated.getIndex(MEMBER_HASH).rows(hash);
186: HashSet set = new HashSet();
187: while (iit.hasNext()) {
188: int r = iit.nextInt();
189: set.add(getTuple(m_aggregated.getInt(r, AGGREGATE)));
190: }
191: return set.iterator();
192: }
193:
194: /**
195: * Get a hashcode that uniquely identifies a particular tuple
196: * @param t the tuple to compute the hash for
197: * @return a unique identifier for the tuple
198: */
199: protected int getHashCode(Tuple t) {
200: // this works for now because hashCode is not overloaded on
201: // the provided Tuple implementations
202: return t.hashCode();
203: }
204:
205: /**
206: * Check a row for validity, optionally throwing an exception when an
207: * invalid row is found.
208: * @param row the row to check
209: * @param throwException indicates if an exception should be thrown when an
210: * invalid row is encountered
211: * @return true if the row was valid, false otherwise
212: */
213: protected boolean validRowCheck(int row, boolean throwException) {
214: if (isValidRow(row)) {
215: return true;
216: } else if (throwException) {
217: throw new IllegalArgumentException("Invalid row value: "
218: + row);
219: } else {
220: return false;
221: }
222: }
223:
224: // ------------------------------------------------------------------------
225: // Table Listener Interception
226:
227: /**
228: * Clear all aggregate mappings for a row when it is deleted.
229: */
230: protected void fireTableEvent(int row0, int row1, int col, int type) {
231: if (col == EventConstants.ALL_COLUMNS
232: && type == EventConstants.DELETE) {
233: for (int r = row0; r <= row1; ++r)
234: clearAggregateMappings(r, false);
235: }
236: super .fireTableEvent(row0, row1, col, type);
237: }
238:
239: // ------------------------------------------------------------------------
240: // Aggregated Iterator
241:
242: /**
243: * Iterator instance that iterates over the items contained in an aggregate.
244: */
245: protected class AggregatedIterator implements Iterator {
246: private IntIterator m_rows;
247: private Tuple m_next = null;
248:
249: public AggregatedIterator(int row) {
250: Index index = m_aggregated.index(AGGREGATE);
251: m_rows = index.rows(row);
252: advance();
253: }
254:
255: public boolean hasNext() {
256: return m_next != null;
257: }
258:
259: public Object next() {
260: Tuple retval = m_next;
261: advance();
262: return retval;
263: }
264:
265: private void advance() {
266: while (m_rows.hasNext()) {
267: int ar = m_rows.nextInt();
268: Tuple t = (Tuple) m_aggregated.get(ar, MEMBER);
269: if (t.isValid()) {
270: m_next = t;
271: return;
272: } else {
273: m_aggregated.removeRow(ar);
274: }
275: }
276: m_next = null;
277: }
278:
279: public void remove() {
280: throw new UnsupportedOperationException();
281: }
282: }
283:
284: // ------------------------------------------------------------------------
285: // Aggregated Table Schema
286:
287: protected static final String AGGREGATE = "aggregate";
288: protected static final String MEMBER_HASH = "hash";
289: protected static final String MEMBER = "member";
290: protected static final Schema AGGREGATED_SCHEMA = new Schema();
291: static {
292: AGGREGATED_SCHEMA.addColumn(AGGREGATE, int.class);
293: AGGREGATED_SCHEMA.addColumn(MEMBER_HASH, int.class);
294: AGGREGATED_SCHEMA.addColumn(MEMBER, Tuple.class);
295: }
296:
297: } // end of class AggregateTable
|