/*
* Copyright (c) 2007 BUSINESS OBJECTS SOFTWARE LIMITED
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of Business Objects nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
/*
* DisplayedGem.java
* Creation date: (02/18/02 6:27:00 PM)
* By: Edward Lam
*/
package org.openquark.gems.client;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.swing.JComponent;
import org.openquark.cal.compiler.CALSourceGenerator;
import org.openquark.cal.compiler.ModuleTypeInfo;
import org.openquark.cal.compiler.Scope;
import org.openquark.cal.compiler.ScopedEntityNamingPolicy;
import org.openquark.cal.compiler.SourceModel;
import org.openquark.cal.valuenode.CALRunner;
import org.openquark.cal.valuenode.Target;
import org.openquark.gems.client.Gem.PartInput;
import org.openquark.gems.client.TableTop.DisplayContext;
import org.openquark.util.EmptyIterator;
import org.openquark.util.Pair;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
/**
* The representation of a gem extended with notions of location and size
* @author Edward Lam
*/
public class DisplayedGem implements Graph.Node {
/** provides context information for calculating display properties of this gem. */
private final DisplayContext displayContext;
/** The gem (model) which is represented by this DisplayedGem. Should be final once assigned. */
private final Gem gem;
/** The body part which you can see */
private final DisplayedPartBody displayedBodyPart;
/** The displayed output part */
private DisplayedPartOutput displayedOutputPart;
/** The array of displayed input parts */
private DisplayedPartInput[] displayedInputParts;
/** Gem location */
private Point location;
/** Gem shape. Only intended to be mutated by implementing class. */
private DisplayedGemShape displayedGemShape;
/** The provider for the displayed gem's shape. */
private final DisplayedGemShape.ShapeProvider shapeProvider;
/**
* Keys for fields in map used with state editable interface
*/
private static final String DISPLAYED_INPUT_PARTS_KEY = "DisplayedInputPartsStateKey";
// Listeners
private DisplayedGemLocationListener gemLocationListener; // listeners for displayed gem location changes
private DisplayedGemSizeListener gemSizeListener; // listeners for displayed gem size changes
/**
* Construct a Gem from an initial location
* @param displayContext provides context information for calculating display properties of this gem
* @param shapeProvider the provider for this gem's shape.
* @param gem Gem the gem (model) which is represented by this DisplayedGem
* @param location Point the initial location of the displayed gem
*/
public DisplayedGem(DisplayContext displayContext, DisplayedGemShape.ShapeProvider shapeProvider, Gem gem, Point location) {
if (displayContext == null || gem == null || location == null) {
throw new NullPointerException();
}
this.displayContext = displayContext;
this.shapeProvider = shapeProvider;
this.gem = gem;
this.location = new Point(location);
// create parts for input, output, and body
displayedBodyPart = new DisplayedPartBody();
Gem.PartOutput outputPartModel = gem.getOutputPart();
if (outputPartModel != null) {
displayedOutputPart = new DisplayedPartOutput(outputPartModel);
}
int nArgs = gem.getNInputs();
displayedInputParts = new DisplayedPartInput[nArgs];
for (int i = 0; i < nArgs; i++) {
displayedInputParts[i] = new DisplayedPartInput(gem.getInputPart(i));
}
}
/**
* Get the gem.
* @return Gem the gem to which this displayed part belongs
*/
public final Gem getGem() {
return gem;
}
/**
* Get the DisplayedGemShape.
* @return the displayedGemShape.
*/
public final DisplayedGemShape getDisplayedGemShape() {
if (displayedGemShape == null) {
displayedGemShape = shapeProvider.getDisplayedGemShape(this);
}
return displayedGemShape;
}
/**
* Get a Shape representing the body shape of this Gem
* @return Shape the body Shape
*/
public final Shape getBodyShape() {
return getDisplayedGemShape().getBodyShape();
}
/**
* Returns the displayed output part.
* @return DisplayedPartOutput
*/
public final DisplayedPartOutput getDisplayedOutputPart() {
return displayedOutputPart;
}
/**
* Returns the displayed input part.
* @param n int the index of the displayed input to return
* @return DisplayedPartInput
*/
public final DisplayedPartInput getDisplayedInputPart(int n) {
return displayedInputParts[n];
}
/**
* Get the displayed body part.
* @return DisplayedPartBody the body part
*/
public final DisplayedPartBody getDisplayedBodyPart() {
return displayedBodyPart;
}
/**
* Get the connectable displayed parts on this gem.
*
* @return List the list of connectable parts on this gem.
* The order is: input parts first (in order), then output part.
*/
public final List<DisplayedPartConnectable> getConnectableDisplayedParts() {
List<DisplayedPartConnectable> connectableParts = new ArrayList<DisplayedPartConnectable>();
if (displayedInputParts != null) {
connectableParts.addAll(Arrays.asList(displayedInputParts));
}
if (displayedOutputPart != null) {
connectableParts.add(displayedOutputPart);
}
return connectableParts;
}
/**
* Get the number of displayed inputs on this gem.
*
* @return int the number of displayed part in this displayed gem
*/
final int getNDisplayedArguments() {
return displayedInputParts == null ? 0 : displayedInputParts.length;
}
/**
* Update the displayed inputs on a displayed gem to reflect any input changes in the underlying gem.
*/
void updateDisplayedInputs() {
int nArgs = gem.getNInputs();
// create a map from input to displayed input for the current displayed inputs
Map<PartInput, DisplayedPartInput> inputDisplayMap = new HashMap<PartInput, DisplayedPartInput>();
int nDisplayedArgs = displayedInputParts.length;
for (int i = 0; i < nDisplayedArgs; i++) {
DisplayedPartInput dInput = displayedInputParts[i];
inputDisplayMap.put(dInput.getPartInput(), dInput);
}
// create an array for the new displayed inputs
DisplayedPartInput[] newDisplayedInputs = new DisplayedPartInput[nArgs];
// update the new displayed inputs based on the updated gem model
for (int i = 0; i < nArgs; i++) {
Gem.PartInput input = gem.getInputPart(i);
DisplayedPartInput dInput = inputDisplayMap.get(input);
// A new input - must create a new displayed input
if (dInput == null) {
dInput = new DisplayedPartInput(input);
}
// update the corresponding new displayed input
newDisplayedInputs[i] = dInput;
}
// switch the parts over
displayedInputParts = newDisplayedInputs;
}
/**
* Set the location of this Gem
* @param newXY Point the new location
*/
public void setLocation(Point newXY) {
// Set the location and delete the cached bounds
if (!location.equals(newXY)) {
location = new Point(newXY);
Rectangle oldBounds = getBounds();
getDisplayedGemShape().purgeCachedBounds();
// notify listeners
if (gemLocationListener != null) {
gemLocationListener.gemLocationChanged(new DisplayedGemLocationEvent(DisplayedGem.this, oldBounds));
}
}
}
/**
* Get the location of this Gem
* @return Point
*/
public final Point getLocation() {
// Get the location and return
return new Point(location);
}
/**
* Get the weighted center point of this Gem's body.
* In mathematics, this is known as the body shape's "centroid".
* @return the centroid of the gem body.
*/
public final Point2D getCenterPoint() {
return getDisplayedGemShape().getCenterPoint();
}
/**
* Is any part of this DisplayedGem under the given point.
* @param xy Point the point to test for
* @return boolean whether we hit
*/
final boolean anyHit(Point xy) {
return whatHit(xy) != null;
}
/**
* Determine what part of this Gem has been hit
* @param xy Point the point to test for
* @return PartConnectable what we hit or null for nothing
*/
final DisplayedPart whatHit(Point xy) {
// Test for body hit
DisplayedGemShape shape = getDisplayedGemShape();
if (shape.bodyHit(xy)) {
return getDisplayedBodyPart();
}
// Test for output arrow hit
if (shape.outputHit(xy)) {
return getDisplayedOutputPart();
}
// Test for input blob hit
int blobNum = shape.inputHit(xy);
if (blobNum >= 0) {
return getDisplayedInputPart(blobNum);
}
// Nothing was hit
return null;
}
/**
* Return the Gem's bounds.
* @return the bounds of the Gem
*/
public final Rectangle getBounds() {
return getDisplayedGemShape().getBounds();
}
/**
* Handle a gem size change.
*/
void sizeChanged() {
Rectangle oldBounds = getBounds();
// Null out the displayed gem shape reference. It will be instantiated the next time it is needed (with fresh bounds..).
displayedGemShape = null;
// notify listeners
if (gemSizeListener != null) {
gemSizeListener.gemSizeChanged(new DisplayedGemSizeEvent(DisplayedGem.this, oldBounds));
}
}
/**
* The textual representation of the gem used for display.
* @return the String to display
*/
public final String getDisplayText() {
if (gem instanceof CodeGem) {
return ((CodeGem)gem).getUnqualifiedName();
} else if (gem instanceof CollectorGem) {
return ((CollectorGem)gem).getUnqualifiedName();
} else if (gem instanceof RecordFieldSelectionGem) {
return ((RecordFieldSelectionGem)gem).getDisplayedText();
} else if (gem instanceof RecordCreationGem) {
return ((RecordCreationGem)gem).getDisplayName();
} else if (gem instanceof FunctionalAgentGem) {
ModuleTypeInfo moduleTypeInfo = displayContext.getContextModuleTypeInfo();
ScopedEntityNamingPolicy namingPolicy = moduleTypeInfo == null ?
namingPolicy = ScopedEntityNamingPolicy.FULLY_QUALIFIED :
new ScopedEntityNamingPolicy.UnqualifiedUnlessAmbiguous(moduleTypeInfo);
return ((FunctionalAgentGem)gem).getGemEntity().getAdaptedName(namingPolicy);
} else if (gem instanceof ReflectorGem) {
return ((ReflectorGem)gem).getUnqualifiedName();
} else if (gem instanceof ValueGem) {
return GemCutter.getResourceString("ValueGemDisplayText");
} else {
throw new IllegalArgumentException("Unknown gem class: " + this.getClass());
}
}
/**
* Describe this Gem
* @return the description
*/
@Override
public final String toString() {
return "DisplayedGem. Location: (" + location + "). Gem: " + gem;
}
/*
* Methods implementing Graph.Node ********************************************************************
*/
/**
* Return the degree of this gem. i.e. how many connections it supports.
* @return double
*/
public final double degree() {
return getConnectableDisplayedParts().size();
}
/**
* Get an iterator over the incoming edges.
* @param validNodes the list of nodes that you would count as incoming edges
* this is used for selective graph traversal
* @return Iterator
*/
public final Iterator<Graph.Edge> getInEdges(List<Graph.Node> validNodes) {
int nDisplayedArgs = getNDisplayedArguments();
if (nDisplayedArgs == 0) {
return EmptyIterator.emptyIterator();
}
List<Graph.Edge> inEdgeList = new ArrayList<Graph.Edge>();
for (int i = 0; i < nDisplayedArgs; ++i) {
DisplayedConnection dConn = getDisplayedInputPart(i).getDisplayedConnection();
if (dConn != null) {
// We only count the edge if the node is valid
if (validNodes.contains(dConn.getSourceNode())) {
inEdgeList.add (dConn);
}
}
}
return inEdgeList.iterator();
}
/**
* Get an iterator over the outgoing edges.
* @param validNodes the list of nodes that you want to count
* @return Iterator
*/
public final Iterator<Graph.Edge> getOutEdges(List<Graph.Node> validNodes) {
// get the output part
DisplayedGem.DisplayedPartOutput partOutput = getDisplayedOutputPart();
// get the connection. If there is no connection or no partOutput, then there are no out edges
DisplayedConnection displayedConnection = partOutput == null ? null : partOutput.getDisplayedConnection();
if (displayedConnection == null) {
return EmptyIterator.emptyIterator();
}
Graph.Node node = displayedConnection.getTargetNode();
// if the node is not valid, then we don't count it
if (!validNodes.contains(node)) {
return EmptyIterator.emptyIterator();
}
List<Graph.Edge> outEdgeList = new ArrayList<Graph.Edge>();
outEdgeList.add (getDisplayedOutputPart().getDisplayedConnection());
return outEdgeList.iterator();
}
/*
* Methods implementing Target ****************************************************************
*/
/**
* @return a target corresponding to this DisplayedGem.
*/
public Target getTarget() {
// if this gem is a Collector Gem then use its name else use the general name
String targetName = (gem instanceof CollectorGem) ? ((CollectorGem)gem).getUnqualifiedName() : CALRunner.TEST_SC_NAME;
return new Target(targetName) {
@Override
protected SourceModel.FunctionDefn getSCDefn(String scName) {
// Build the new SC definition and return
return CALSourceGenerator.getFunctionSourceModel(scName, gem, Scope.PRIVATE);
}
};
}
/**
* Return the arguments as would be required by current definition of the Gem tree at the Target.
* @return the list of arguments required by the target.
*/
public final List<PartInput> getTargetArguments() {
return gem.getTargetInputs();
}
/*
* Methods supporting XMLPersistable ********************************************
*/
/**
* Attach the saved form of this object as a child XML node.
* @param parentNode Node the node that will be the parent of the generated XML.
* The generated XML will be appended as a subtree of this node.
* Note: parentNode must be a node type that can accept children (eg. an Element or a DocumentFragment)
* @param gemContext the context in which the gem is saved.
*/
public void saveXML(Node parentNode, GemContext gemContext) {
Document document = (parentNode instanceof Document) ? (Document)parentNode : parentNode.getOwnerDocument();
// Create the displayed gem element
Element resultElement = document.createElement(GemPersistenceConstants.DISPLAYED_GEM_TAG);
parentNode.appendChild(resultElement);
// Add an element for the info for the underlying gem.
gem.saveXML(resultElement, gemContext);
// Add a child location element
Element locationElement = GemCutterPersistenceHelper.pointToElement(getLocation(), document);
resultElement.appendChild(locationElement);
}
/*
* Methods supporting undo.StateEditable ********************************************
*/
/**
* Restore the stored displayed input state.
* @param state Hashtable the stored state
*/
void restoreDisplayedInputState(Hashtable<Pair<DisplayedGem, String>, DisplayedPartInput[]> state) {
DisplayedPartInput[] displayedInputParts =
state.get(new Pair<DisplayedGem, String>(DisplayedGem.this, DISPLAYED_INPUT_PARTS_KEY));
if (displayedInputParts != null) {
this.displayedInputParts = displayedInputParts.clone();
}
}
/**
* Save the displayed input state.
* @param state Hashtable the table in which to store the displayed input state
*/
void storeDisplayedInputState(Hashtable<Pair<DisplayedGem, String>, DisplayedPartInput[]> state) {
state.put(new Pair<DisplayedGem, String>(DisplayedGem.this, DISPLAYED_INPUT_PARTS_KEY), displayedInputParts.clone());
}
/*
* Methods to handle listeners ****************************************************************
*/
/**
* Adds the specified location change listener to receive location change events from this gem .
* If l is null, no exception is thrown and no action is performed.
*
* @param l the location listener.
*/
public synchronized void addLocationChangeListener(DisplayedGemLocationListener l) {
if (l == null) {
return;
}
gemLocationListener = GemEventMulticaster.add(gemLocationListener, l);
}
/**
* Removes the specified location change listener so that it no longer receives location change events from this gem.
* This method performs no function, nor does it throw an exception, if the listener specified by
* the argument was not previously added to this component.
* If l is null, no exception is thrown and no action is performed.
*
* @param l the location listener.
*/
public synchronized void removeLocationChangeListener(DisplayedGemLocationListener l) {
if (l == null) {
return;
}
gemLocationListener = GemEventMulticaster.remove(gemLocationListener, l);
}
/**
* Adds the specified size change listener to receive size change events from this gem .
* If l is null, no exception is thrown and no action is performed.
*
* @param l the size listener.
*/
public synchronized void addSizeChangeListener(DisplayedGemSizeListener l) {
if (l == null) {
return;
}
gemSizeListener = GemEventMulticaster.add(gemSizeListener, l);
}
/**
* Removes the specified size change listener so that it no longer receives size change events from this gem.
* This method performs no function, nor does it throw an exception, if the listener specified by
* the argument was not previously added to this component.
* If l is null, no exception is thrown and no action is performed.
*
* @param l the size listener.
*/
public synchronized void removeSizeChangeListener(DisplayedGemSizeListener l) {
if (l == null) {
return;
}
gemSizeListener = GemEventMulticaster.remove(gemSizeListener, l);
}
/**
* A part of a Gem
* @author Edward Lam
*/
public abstract class DisplayedPart {
/**
* Default constructor for a DisplayedPart
*/
public DisplayedPart() {
}
/**
* Get the gem to which this displayed part belongs.
* @return Gem the gem to which this displayed part belongs
*/
public Gem getGem() {
return gem;
}
/**
* Get the displayed gem of which this is part
* @return DisplayedGem the displayed gem to which this displayed part belongs
*/
public DisplayedGem getDisplayedGem() {
return DisplayedGem.this;
}
/**
* Get the bounds of this part
* @return Rectangle the bounds
*/
public abstract Rectangle getBounds();
}
/**
* A connectable PartConnectable has both a type and a bind point for a Connector.
* @author Edward Lam
*/
public abstract class DisplayedPartConnectable extends DisplayedPart {
/** the PartConnectable represented by this DisplayedPartConnectable */
private final Gem.PartConnectable partConnectable;
/** the displayed connection associated with this connectable part*/
private DisplayedConnection dConnection;
/**
* Default constructor for a DisplayedPartConnectable
*/
DisplayedPartConnectable(Gem.PartConnectable partConnectable) {
super();
this.partConnectable = partConnectable;
}
/**
* Get the connectable part.
* @return Gem.PartConnectable the part to which this displayed part belongs
*/
public final Gem.PartConnectable getPartConnectable() {
return partConnectable;
}
/**
* Get the connection point for this part
* @return the point at which components will connect
*/
public abstract Point getConnectionPoint();
/**
* Repaint this PartConnectable's connection (if it has one!)
* @param c JComponent the component to paint on
*/
public void repaintConnection(JComponent c) {
if (isConnected()) {
c.repaint(dConnection.getBounds());
}
}
/**
* Bind a connection to this part.
* @param dConn DisplayedConnection the connection to connect
*/
public void bindDisplayedConnection(DisplayedConnection dConn) {
dConnection = dConn;
}
/**
* Get the displayed connection at this part.
* @return conn DisplayedConnection the connection bound to this part
*/
public final DisplayedConnection getDisplayedConnection() {
return dConnection;
}
/**
* Check if this part is connected.
* @return boolean whether it is already connected
*/
public final boolean isConnected() {
return (dConnection != null);
}
/**
* Purge this PartConnectable's connection route (if it has one)
*/
public void purgeConnectionRoute() {
if (dConnection != null) {
dConnection.purgeRoute();
}
}
/**
* Get the source of this edge.
* @return Graph.Node
*/
public final Graph.Node getSourceNode() {
if (isConnected()) {
return dConnection.getSourceNode();
}
return null;
}
/**
* Get the destination of this edge.
* @return Graph.Node
*/
public final Graph.Node getTargetNode() {
if (isConnected()) {
return dConnection.getTargetNode();
}
return null;
}
}
/**
* The PartConnectable representing the body of the Gem
* @author Edward Lam
*/
public class DisplayedPartBody extends DisplayedPart {
/**
* Default constructor for a DisplayedPartBody
*/
private DisplayedPartBody() {
}
/**
* Get the bounds of this part
* @return Rectangle the bounds
*/
@Override
public final Rectangle getBounds() {
return getDisplayedGemShape().getBodyBounds();
}
}
/**
* A PartInput is a connectable part which is a sink (destination).
* @author Edward Lam
*/
public class DisplayedPartInput extends DisplayedPartConnectable {
/**
* Default constructor for a DisplayedPartInput
* @param partInput the input corresponding to this displayed input.
*/
public DisplayedPartInput(Gem.PartInput partInput) {
super(partInput);
}
/**
* Get the input part.
* @return Gem.PartInput the part to which this displayed part belongs
*/
public final Gem.PartInput getPartInput() {
return (Gem.PartInput)getPartConnectable();
}
/**
* Get the connection point for this part
* @return the point at which components will connect
*/
@Override
public Point getConnectionPoint() {
return getDisplayedGemShape().getInputConnectPoint(getPartInput().getInputNum());
}
/**
* Get the bounds of this part foo.
* @return Rectangle the bounds
*/
@Override
public Rectangle getBounds() {
return getDisplayedGemShape().getInBounds();
}
}
/**
* A PartOutput is a connectable part which is a source/output.
* @author Edward Lam
*/
public class DisplayedPartOutput extends DisplayedPartConnectable {
/**
* Default constructor for a DisplayedPartOutput
* @param partOutput the output corresponding to this displayed output.
*/
DisplayedPartOutput(Gem.PartOutput partOutput) {
super(partOutput);
}
/**
* Get the output part.
* @return Gem.PartOutput the part to which this displayed part belongs
*/
public final Gem.PartOutput getPartOutput() {
return (Gem.PartOutput)getPartConnectable();
}
/**
* Get the connection point for this part
* @return the point at which components will connect
*/
@Override
public Point getConnectionPoint() {
return getDisplayedGemShape().getOutputConnectPoint();
}
/**
* Get the bounds of this part
* @return Rectangle the bounds
*/
@Override
public Rectangle getBounds() {
return getDisplayedGemShape().getOutBounds();
}
}
}