/** * Copyright (c) 2004-2006 Regents of the University of California. * See "license-prefuse.txt" for licensing terms. */ package prefuse.visual; import java.util.HashSet; import java.util.Iterator; import prefuse.Visualization; import prefuse.data.Schema; import prefuse.data.Table; import prefuse.data.Tuple; import prefuse.data.event.EventConstants; import prefuse.data.util.Index; import prefuse.util.collections.IntIterator; import prefuse.visual.tuple.TableAggregateItem; /** * VisualTable instance that maintains visual items representing aggregates * of items. This class maintains both a collection of AggregateItems and * a mapping between AggregateItems and the VisualItems contained within * those aggregates. * * @author <a href="http://jheer.org">jeffrey heer</a> */ public class AggregateTable extends VisualTable { /** * Table storing the 1->Many aggregation mappings */ protected Table m_aggregated; /** * Create a new AggregateTable. * @param vis the Visualization associated with the table * @param group the data group the table contents belongs to */ public AggregateTable(Visualization vis, String group) { this(vis, group, VisualItem.SCHEMA); } /** * Create a new AggregateTable. * @param vis the Visualization associated with the table * @param group the data group the table contents belongs to * @param schema the Schema to use for this table */ public AggregateTable(Visualization vis, String group, Schema schema) { super(vis, group, schema, TableAggregateItem.class); m_aggregated = AGGREGATED_SCHEMA.instantiate(); m_aggregated.index(AGGREGATE); m_aggregated.index(MEMBER_HASH); } // ------------------------------------------------------------------------ /** * Get the size of the aggregate represented at the given table row. * Returns the number of visual items contained in the aggregation. * @return the aggregate size for the given row */ public int getAggregateSize(int row) { int size = 0; AggregatedIterator ati = new AggregatedIterator(row); for ( ; ati.hasNext(); ++size, ati.next() ); return size; } /** * Add an item to the aggregation at the given row. * @param row the row index of the aggregate * @param member the item to add to the aggregation */ public void addToAggregate(int row, VisualItem member) { validRowCheck(row, true); if ( !aggregateContains(row, member) ) { int ar = m_aggregated.addRow(); m_aggregated.setInt(ar, AGGREGATE, row); m_aggregated.setInt(ar, MEMBER_HASH, getHashCode(member)); m_aggregated.set(ar, MEMBER, member); fireTableEvent(row, row, EventConstants.ALL_COLUMNS, EventConstants.UPDATE); } } /** * Remove an item from the aggregation at the given row * @param row the row index of the aggregate * @param member the item to remove from the aggregation */ public void removeFromAggregate(int row, VisualItem member) { validRowCheck(row, true); int ar = getAggregatedRow(row, member); if ( ar >= 0 ) { m_aggregated.removeRow(ar); fireTableEvent(row, row, EventConstants.ALL_COLUMNS, EventConstants.UPDATE); } } /** * Remove all items contained in the aggregate at the given row * @param row the row index of the aggregate */ public void removeAllFromAggregate(int row) { clearAggregateMappings(row, true); } /** * Clears all aggregates mappings for the aggregate at the given row, * optionally issuing a table update. * @param row the table row of the aggregate * @param update indicates whether or not to fire a table update */ protected void clearAggregateMappings(int row, boolean update) { Index index = m_aggregated.index(AGGREGATE); boolean fire = false; for ( IntIterator rows = index.rows(row); rows.hasNext(); ) { int r = rows.nextInt(); // this removal maneuver is ok because we know we are // pulling row values directly from an index // with intervening iterators, remove might throw an exception rows.remove(); m_aggregated.removeRow(r); fire = true; } if ( update && fire ) fireTableEvent(row, row, EventConstants.ALL_COLUMNS, EventConstants.UPDATE); } /** * Indicates if an item is a member of the aggregate at the given row * @param row the table row of the aggregate * @param member the item to check from containment * @return true if the item is in the aggregate, false otherwise */ public boolean aggregateContains(int row, VisualItem member) { return getAggregatedRow(row, member) >= 0; } /** * Get the row index to the aggregate mapping table for the given * aggregate and contained VisualItem. * @param row the table row of the aggregate * @param member the VisualItem to look up * @return the row index into the internal aggregate mapping table for the * mapping between the given aggregate row and given VisualItem */ protected int getAggregatedRow(int row, VisualItem member) { Index index = m_aggregated.index(MEMBER_HASH); int hash = getHashCode(member); int ar = index.get(hash); if ( ar < 0 ) { return -1; } else if ( m_aggregated.getInt(ar, AGGREGATE) == row ) { return ar; } else { for ( IntIterator rows = index.rows(hash); rows.hasNext(); ) { ar = rows.nextInt(); if ( m_aggregated.getInt(ar, AGGREGATE) == row ) return ar; } return -1; } } /** * Get all VisualItems within the aggregate at the given table row. * @param row the table row of the aggregate * @return an iterator over the items in the aggregate */ public Iterator aggregatedTuples(int row) { return new AggregatedIterator(row); } /** * Get an iterator over all AggregateItems that contain the given Tuple. * @param t the input tuple * @return an iterator over all AggregateItems that contain the input Tuple */ public Iterator getAggregates(Tuple t) { int hash = getHashCode(t); IntIterator iit = m_aggregated.getIndex(MEMBER_HASH).rows(hash); HashSet set = new HashSet(); while ( iit.hasNext() ) { int r = iit.nextInt(); set.add(getTuple(m_aggregated.getInt(r, AGGREGATE))); } return set.iterator(); } /** * Get a hashcode that uniquely identifies a particular tuple * @param t the tuple to compute the hash for * @return a unique identifier for the tuple */ protected int getHashCode(Tuple t) { // this works for now because hashCode is not overloaded on // the provided Tuple implementations return t.hashCode(); } /** * Check a row for validity, optionally throwing an exception when an * invalid row is found. * @param row the row to check * @param throwException indicates if an exception should be thrown when an * invalid row is encountered * @return true if the row was valid, false otherwise */ protected boolean validRowCheck(int row, boolean throwException) { if ( isValidRow(row) ) { return true; } else if ( throwException ) { throw new IllegalArgumentException("Invalid row value: "+row); } else { return false; } } // ------------------------------------------------------------------------ // Table Listener Interception /** * Clear all aggregate mappings for a row when it is deleted. */ protected void fireTableEvent(int row0, int row1, int col, int type) { if ( col==EventConstants.ALL_COLUMNS && type==EventConstants.DELETE ) { for ( int r=row0; r<=row1; ++r ) clearAggregateMappings(r, false); } super.fireTableEvent(row0, row1, col, type); } // ------------------------------------------------------------------------ // Aggregated Iterator /** * Iterator instance that iterates over the items contained in an aggregate. */ protected class AggregatedIterator implements Iterator { private IntIterator m_rows; private Tuple m_next = null; public AggregatedIterator(int row) { Index index = m_aggregated.index(AGGREGATE); m_rows = index.rows(row); advance(); } public boolean hasNext() { return m_next != null; } public Object next() { Tuple retval = m_next; advance(); return retval; } private void advance() { while ( m_rows.hasNext() ) { int ar = m_rows.nextInt(); Tuple t = (Tuple)m_aggregated.get(ar, MEMBER); if ( t.isValid() ) { m_next = t; return; } else { m_aggregated.removeRow(ar); } } m_next = null; } public void remove() { throw new UnsupportedOperationException(); } } // ------------------------------------------------------------------------ // Aggregated Table Schema protected static final String AGGREGATE = "aggregate"; protected static final String MEMBER_HASH = "hash"; protected static final String MEMBER = "member"; protected static final Schema AGGREGATED_SCHEMA = new Schema(); static { AGGREGATED_SCHEMA.addColumn(AGGREGATE, int.class); AGGREGATED_SCHEMA.addColumn(MEMBER_HASH, int.class); AGGREGATED_SCHEMA.addColumn(MEMBER, Tuple.class); } } // end of class AggregateTable