/* * @(#)AbstractCellView.java 1.0 03-JUL-04 * * Copyright (c) 2001-2005 Gaudenz Alder * * See LICENSE file in distribution for licensing details of this source file */ package org.jgraph.graph; import java.awt.Component; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.io.Serializable; import java.util.ArrayList; import java.util.Iterator; import java.util.LinkedList; import java.util.Map; import java.util.Stack; import javax.swing.tree.DefaultMutableTreeNode; import org.jgraph.JGraph; /** * The abstract base class for all cell views. * * @version 1.0 1/3/02 * @author Gaudenz Alder */ @SuppressWarnings ( "all" ) public abstract class AbstractCellView implements CellView, Serializable { /** Editor for the cell. */ public static transient GraphCellEditor cellEditor; // Headless environment may have no default fonts installed static { try { cellEditor = new DefaultGraphCellEditor(); } catch (Error e) { // No cell editor } } /** Reference to the cell for this view */ protected Object cell = null; /** Cached parent view */ protected CellView parent = null; /** Cached child views. Default is a ArrayList with allocation size 0. */ protected java.util.List childViews = new ArrayList(0); /** * Contains the complete set of attributes, including the cell's attributes. * The values in this map are overriden by the corresponding values in * <code>attributes</code>. */ protected AttributeMap allAttributes = createAttributeMap(); /** * Hashtable for attributes. Value in this map override the values in * <code>allAttributes</code>. */ protected AttributeMap attributes = allAttributes; /** Cached bounds of all children if vertex is a group */ protected transient Rectangle2D groupBounds = VertexView.defaultBounds; /** * Constructs an empty abstract cell view. You should set a cell on this * view using setCell before doing anything. Optionally you can also set a * different attribute map using setAttributeMap. Note: To change the * attribute map you should now use the changeAttributes method. */ public AbstractCellView() { } /** * Constructs a view for the specified model object, and invokes update on * the new instance. * * @param cell * reference to the model object */ public AbstractCellView(Object cell) { setCell(cell); } /** * Hook for subclassers to avoid creating an empty AttributeMap during * construction of the instance. Override this and return null if you want * to avoid creation of an attribute map at construction time. */ protected AttributeMap createAttributeMap() { return new AttributeMap(); } /** * Returns the model object that this view represents. * * @return the model object that this view represents */ public Object getCell() { return cell; } /** * Sets the model object that this view represents to the specified cell * * @param cell * the model object this view will represent */ public void setCell(Object cell) { this.cell = cell; } /** * Create child views and reload properties for this view. Invokes update * first. * * @param cache * the graph model to be used * @param mapper * the cell mapper to be used * @param createDependentViews * whether or not to create a view if one does not already exist */ public void refresh(GraphLayoutCache cache, CellMapper mapper, boolean createDependentViews) { // Re-read global attributes GraphModel model = cache.getModel(); allAttributes = getCellAttributes(model); // Cache Parent View if (mapper != null && model != null) { // Create parent only if it's visible in the graph Object par = model.getParent(cell); CellView tmp = mapper.getMapping(par, createDependentViews); if (tmp != parent) removeFromParent(); parent = tmp; } // Cache Cell Attributes in View update(cache); // Re-load Child Views childViews.clear(); for (int i = 0; i < model.getChildCount(cell); i++) { Object child = model.getChild(cell, i); CellView view = mapper.getMapping(child, createDependentViews); if (!model.isPort(child) && view != null) childViews.add(view); } } /** * Hook for subclassers to avoid cloning the cell's attributes. Return * model.getAttributes(cell) to avoid cloning. */ protected AttributeMap getCellAttributes(GraphModel model) { return (AttributeMap) model.getAttributes(cell).clone(); } /** * Update attributes for this view and indicate to the parent this child has * been updated */ public void update(GraphLayoutCache cache) { mergeAttributes(); // Notify Parent groupBounds = null; childUpdated(); } /** * Implements the merging of the cell's attributes, initially stored in * allAttributes, and the location attributes. The result should be stored * in allAttributes. This hook is for subclassers to change the merging * strategy. */ protected void mergeAttributes() { allAttributes.putAll(attributes); } /** * Indicates to parent, if any, that this child has been updated. */ public void childUpdated() { if (parent != null) parent.childUpdated(); groupBounds = null; } // // Graph Structure // /** * Returns the parent view for this view. * * @return the parent view for this view */ public CellView getParentView() { return parent; } /** * Returns the child views of this view. * * @return the child views of this view */ public CellView[] getChildViews() { CellView[] array = new CellView[childViews.size()]; childViews.toArray(array); return array; } /** * Returns all views, including descendants that have a parent in * <code>views</code> without the PortViews. Note: Iterative * Implementation using view.getChildViews. This returns the array in * inverse order, ie with the top most cell view at index 0. * * @param views * the cell views whose descendants are to be returned * @return the specified views and all their descendant views */ public static CellView[] getDescendantViews(CellView[] views) { Stack stack = new Stack(); for (int i = 0; i < views.length; i++) stack.add(views[i]); ArrayList result = new ArrayList(); while (!stack.isEmpty()) { CellView tmp = (CellView) stack.pop(); Object[] children = tmp.getChildViews(); for (int i = 0; i < children.length; i++) stack.add(children[i]); result.add(tmp); } CellView[] ret = new CellView[result.size()]; result.toArray(ret); return ret; } /** * Removes this view from the list of children of the parent. */ public void removeFromParent() { if (parent instanceof AbstractCellView) { java.util.List list = ((AbstractCellView) parent).childViews; // TODO performance could be quadratic list.remove(this); } } /** * Returns <code>true</code> if the view is a leaf. * * @return <code>true</code> if the view is a leaf */ public boolean isLeaf() { return childViews.isEmpty(); } // // View Attributes // /** * Return the attributes of the view. * * @return the <code>attributes</code> of this view */ public AttributeMap getAttributes() { return attributes; } /** * Sets the attributes of this view to the specified value * * @param attributes * the new attributes to set */ public void setAttributes(AttributeMap attributes) { this.attributes = attributes; } /** * Returns the attributes of the view combined with the attributes of the * corresponding cell. The view's attributes override the cell's attributes * with the same key. */ public AttributeMap getAllAttributes() { return allAttributes; } /** * Applies <code>change</code> to the attributes of the view and calls * update. * * @param change * a map of attribute changes to apply * @return the undo map that reverses this change */ public Map changeAttributes(GraphLayoutCache cache, Map change) { if (change != null) { Map undo = attributes.applyMap(change); update(cache); return undo; } return null; } // // View Methods // /** * Returns the cached bounds for the group if isleaf is false */ public Rectangle2D getBounds() { if (!isLeaf()) { if (groupBounds == null) updateGroupBounds(); return groupBounds; } return null; } /** * Returns the bounding box for the specified views. * * @param views * the views for whom the bounding box is to be determined * @return the bounding box of the specified views */ public static Rectangle2D getBounds(CellView[] views) { if (views != null && views.length > 0) { Rectangle2D ret = null; for (int i = 0; i < views.length; i++) { if (views[i] != null) { Rectangle2D r = views[i].getBounds(); if (r != null) { if (ret == null) ret = new Rectangle2D.Double(r.getX(), r.getY(), r .getWidth(), r.getHeight()); else Rectangle2D.union(ret, r, ret); } } } return ret; } return null; } /** * Sets the bounds of this <code>view</code>. Calls translateView and * scaleView. * * @param bounds * the new bounds for this cell view */ public void setBounds(Rectangle2D bounds) { Rectangle2D oldBounds = getBounds(); if (oldBounds == null) oldBounds = new Rectangle2D.Double(); Point2D p0 = new Point2D.Double(oldBounds.getX(), oldBounds.getY()); Point2D pe = new Point2D.Double(bounds.getX(), bounds.getY()); Rectangle2D localBounds = new Rectangle2D.Double(bounds.getX(), bounds .getY(), bounds.getWidth(), bounds.getHeight()); if (GraphConstants.isMoveable(getAllAttributes()) && !pe.equals(p0)) translate(pe.getX() - p0.getX(), pe.getY() - p0.getY()); else localBounds.setFrame(localBounds.getX(), localBounds.getY(), bounds .getWidth() - pe.getX() + p0.getX(), bounds.getHeight() - pe.getY() + p0.getY()); double lbw = localBounds.getWidth(), lbh = localBounds.getHeight(); double obw = oldBounds.getWidth(), obh = oldBounds.getHeight(); if ((lbw != obw || lbh != obh) && obw > 0 && obh > 0) scale(lbw / obw, lbh / obh, pe); } /** * Updates the bounds of this view and its children * */ protected void updateGroupBounds() { // Note: Prevent infinite recursion by removing // child edges that point to their parent. CellView[] childViews = getChildViews(); LinkedList result = new LinkedList(); for (int i = 0; i < childViews.length; i++) if (includeInGroupBounds(childViews[i])) result.add(childViews[i]); childViews = new CellView[result.size()]; result.toArray(childViews); Rectangle2D r = getBounds(childViews); int groupBorder = GraphConstants.getInset(getAllAttributes()); if (r != null) r.setFrame(r.getX() - groupBorder, r.getY() - groupBorder, r .getWidth() + 2 * groupBorder, r.getHeight() + 2 * groupBorder); groupBounds = r; } /** * This is used to exclude certain cell views from the group bounds * computation. This implementation returns false for edges that connect to * one of their ancestor groups (eg. parent). * * @param view * the cell view to be included in the group bounds or not * @return whether or not to include the specified cell in the group bounds */ protected boolean includeInGroupBounds(CellView view) { if (view instanceof EdgeView && getCell() instanceof DefaultMutableTreeNode) { EdgeView edgeView = (EdgeView) view; if (edgeView.getCell() instanceof DefaultMutableTreeNode) { DefaultMutableTreeNode edge = (DefaultMutableTreeNode) edgeView .getCell(); Object src = null; if (edgeView.getSource() != null && edgeView.getSource().getParentView() != null) src = edgeView.getSource().getParentView().getCell(); else if (edgeView.getSourceParentView() != null) src = edgeView.getSourceParentView().getCell(); if (src instanceof DefaultMutableTreeNode) { DefaultMutableTreeNode source = (DefaultMutableTreeNode) src; if (source.isNodeDescendant(edge)) return false; } Object tgt = null; if (edgeView.getTarget() != null && edgeView.getTarget().getParentView() != null) tgt = edgeView.getTarget().getParentView().getCell(); else if (edgeView.getTargetParentView() != null) tgt = edgeView.getTargetParentView().getCell(); if (tgt instanceof DefaultMutableTreeNode) { DefaultMutableTreeNode target = (DefaultMutableTreeNode) tgt; if (target.isNodeDescendant(edge)) { return false; } } } } return true; } /** * Translates <code>view</code> (group) by <code>dx, dy</code>. * * @param dx * the x-coordinate amount to translate by * @param dy * the y-coordinate amount to translate by */ public void translate(double dx, double dy) { if (isLeaf()) getAllAttributes().translate(dx, dy); else { int moveableAxis = GraphConstants .getMoveableAxis(getAllAttributes()); if (moveableAxis == GraphConstants.X_AXIS) dy = 0; else if (moveableAxis == GraphConstants.Y_AXIS) dx = 0; Iterator it = childViews.iterator(); while (it.hasNext()) { Object view = it.next(); if (view instanceof AbstractCellView) { AbstractCellView child = (AbstractCellView) view; child.translate(dx, dy); } } } } /** * Scale <code>view</code> (group) by <code>sx, sy</code>. * * @param sx * the multiple by which the x coordinate position of the cell * view is to be scaled * @param sy * the multiple by which the y coordinate position of the cell * view is to be scaled * @param origin * the origin point from which the scaling will calculate */ public void scale(double sx, double sy, Point2D origin) { if (isLeaf()) getAttributes().scale(sx, sy, origin); else { int sizeableAxis = GraphConstants .getSizeableAxis(getAllAttributes()); if (sizeableAxis == GraphConstants.X_AXIS) sy = 1; else if (sizeableAxis == GraphConstants.Y_AXIS) sx = 1; Iterator it = childViews.iterator(); while (it.hasNext()) { Object view = it.next(); if (view instanceof AbstractCellView) { AbstractCellView child = (AbstractCellView) view; Map attrs = child.getAttributes(); if (GraphConstants.isSizeable(attrs) || GraphConstants.isAutoSize(attrs)) child.scale(sx, sy, origin); } } } } /** * Returns true if the view intersects the given rectangle. * * @param graph * the <code>JGraph</code> instance of the view * @param rect * the rectangle within which intersection is being checked for * * @return whether or not the rectangle specified intersects the view */ public boolean intersects(JGraph graph, Rectangle2D rect) { if (isLeaf() || GraphConstants.isGroupOpaque(getAllAttributes())) { Rectangle2D bounds = getBounds(); if (bounds != null) return bounds.intersects(rect); } else { // Check If Children Intersect Iterator it = childViews.iterator(); while (it.hasNext()) if (((CellView) it.next()).intersects(graph, rect)) return true; } return false; } // // View Editors // /** * Returns a renderer component, configured for the view. The method used to * obtain the renderer instance must install the necessary attributes from * this view * * @param graph * the <code>JGraph</code> instance of the view * @param selected * whether or not this view is selected * @param focus * whether or not this view is the focus * @param preview * whether or not it is a preview of the view * * @return the renderer component for this view with this views attributes * installed */ public Component getRendererComponent(JGraph graph, boolean selected, boolean focus, boolean preview) { CellViewRenderer cvr = getRenderer(); if (cvr != null) return cvr.getRendererComponent(graph, this, selected, focus, preview); return null; } /** * Obtains the renderer instance for this view * * @return the renderer instance for this view */ public abstract CellViewRenderer getRenderer(); /** * Returns a cell handle for the view. * * @param context * the context of this cell view (cells indirectly affected by * it) * @return the cell handle for this view */ public abstract CellHandle getHandle(GraphContext context); /** * Returns a cell editor for the view. * * @return the cell editor for this view */ public GraphCellEditor getEditor() { return cellEditor; } public static Point2D getCenterPoint(CellView vertex) { Rectangle2D r = vertex.getBounds(); if (r != null) return new Point2D.Double(r.getCenterX(), r.getCenterY()); return null; } /** * Returns the intersection of the bounding rectangle and the straight line * between the source and the specified point p. The specified point is * expected not to intersect the bounds. Note: You must override this method * if you use a different renderer. This is because this method relies on * the VertexRenderer interface, which can not be safely assumed for * subclassers. */ public Point2D getPerimeterPoint(EdgeView edge, Point2D source, Point2D p) { return getCenterPoint(this); } }