/*
* 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.
*/
/*
* DisplayedGemShape.java
* Creation date: Mar 26, 2004.
* By: Edward Lam
*/
package org.openquark.gems.client;
import java.awt.Dimension;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Polygon;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Point2D;
import java.awt.geom.RoundRectangle2D;
import java.awt.image.BufferedImage;
import java.util.List;
/**
* A DisplayedGemShape represents the shape/morphology/appearance of a displayed gem on the TableTop.
* @author Edward Lam
*/
public abstract class DisplayedGemShape {
/** An image from which to create an unscaled graphics context. */
private static BufferedImage dummyImage = new BufferedImage(1,1,BufferedImage.TYPE_INT_ARGB);
/** A Rendering hints object that directs a graphics object to render at the highest quality. */
private static final RenderingHints RENDERING_HINTS_HIGH_QUALITY = new RenderingHints(null);
static {
RENDERING_HINTS_HIGH_QUALITY.put(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
RENDERING_HINTS_HIGH_QUALITY.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
RENDERING_HINTS_HIGH_QUALITY.put(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
RENDERING_HINTS_HIGH_QUALITY.put(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_ENABLE);
RENDERING_HINTS_HIGH_QUALITY.put(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
RENDERING_HINTS_HIGH_QUALITY.put(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
RENDERING_HINTS_HIGH_QUALITY.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
RENDERING_HINTS_HIGH_QUALITY.put(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
}
/** The displayed gem represented by this shape. */
protected final DisplayedGem displayedGem;
/** Information on whatever is supposed to fit inside the shape. */
protected final InnerComponentInfo innerComponentInfo;
// Cached members
private transient Rectangle cachedBounds; // Cached overall bounds of the Gem
private transient Rectangle cachedBodyBounds; // Cached body area bounds of the Gem
/**
* An InnerComponentInfo contains information on the image which is supposed to fit inside the shape representing the displayed gem.
* @author Edward Lam
*/
interface InnerComponentInfo {
/**
* Get the bounds of the inner component.
* @return the bounds.
*/
Rectangle getBounds();
/**
* Get the bounds of the inner component where there are multiple name labels drawn
* @return a list of rectangles or null if not applicable.
*/
List<Rectangle> getInputNameLabelBounds();
/**
* Paint the inner component in the displayed gem.
* @param tableTop
* @param g2d
*/
void paint(TableTop tableTop, Graphics2D g2d);
}
/**
* A ShapeProvider indicates a shape for a displayed gem.
* @author Edward Lam
*/
interface ShapeProvider {
/**
* Get the displayed gem shape.
* @param displayedGem the displayed gem for which the shape should be provided.
* @return the displayed gem's shape.
*/
public DisplayedGemShape getDisplayedGemShape(DisplayedGem displayedGem);
}
/**
* A DisplayedGemShape representing an Oval/Pill shape.
* @author Edward Lam
*/
public static class Oval extends DisplayedGemShape {
/**
* Constructor for a Oval DisplayedGemShape.
* @param displayedGem
*/
Oval(DisplayedGem displayedGem) {
super(displayedGem, TableTopGemPainter.getBoldNameInfo(displayedGem));
}
/**
* {@inheritDoc}
*/
@Override
public Point getInputConnectPoint(int inputIndex) {
if (inputIndex == 0 && canHaveInputs(displayedGem)) {
// The input is in the middle of the left edge..
Rectangle inRect = getInBounds();
int centreY = (int)inRect.getCenterY();
return new Point(inRect.x, centreY);
} else {
return null;
}
}
/**
* {@inheritDoc}
*/
@Override
public Shape getBodyShape() {
Rectangle bodyBounds = getBodyBounds();
// The main Gem rectangle - a rectangle with round corners
double arcSize = bodyBounds.height / 2.0;
Shape bodyShape = new RoundRectangle2D.Double(bodyBounds.getX(), bodyBounds.getY(),
bodyBounds.getWidth() - 1.0, bodyBounds.getHeight() - 1.0,
arcSize, arcSize);
return bodyShape;
}
/**
* {@inheritDoc}
*/
@Override
public Dimension getDimensions() {
// Get the graphics context for the context we will be drawing into
Graphics g = getGraphics();
// The X dimension is based on the size of the text for the name, plus some margins
g.setFont(GemCutterPaintHelper.getBoldFont());
FontMetrics fm = g.getFontMetrics();
// Calculate width
int connectingAreaWidth = 0;
if (canHaveInputs(displayedGem)) {
connectingAreaWidth += DisplayConstants.INPUT_AREA_WIDTH;
}
if (hasOutput(displayedGem)) {
connectingAreaWidth += DisplayConstants.OUTPUT_AREA_WIDTH;
}
int gemWidth = fm.stringWidth(displayedGem.getDisplayText()) + 2 * DisplayConstants.LET_LABEL_MARGIN + connectingAreaWidth;
// Calculate height
int gemHeight = DisplayConstants.ARGUMENT_SPACING;
g.dispose();
// Return the dimensions
return new Dimension(gemWidth, gemHeight);
}
}
/**
* A DisplayedGemShape representing an Triangle shape.
* @author Edward Lam
*/
public static class Triangular extends DisplayedGemShape {
/**
* Constructor for a Triangular DisplayedGemShape.
* @param displayedGem
*/
Triangular(DisplayedGem displayedGem) {
super(displayedGem, TableTopGemPainter.getNameTapeLabelInfo(displayedGem));
}
/**
* {@inheritDoc}
*/
@Override
public Point getInputConnectPoint(int arg) {
Rectangle inRect = getInBounds();
int y = inRect.y + DisplayConstants.ARGUMENT_SPACING * (arg + 1);
return new Point(inRect.x, y);
}
/**
* {@inheritDoc}
*/
@Override
public Dimension getDimensions() {
// Get the graphics context for the context we will be drawing into
Graphics g = getGraphics();
// The X dimension is based on the size of the text for the name, plus some margins
g.setFont(GemCutterPaintHelper.getTitleFont());
FontMetrics fm = g.getFontMetrics();
g.dispose();
// Calculate width
String displayText = displayedGem.getDisplayText();
int gemWidth = fm.stringWidth(displayText) + 8 * DisplayConstants.INPUT_OUTPUT_LABEL_MARGIN + DisplayConstants.BEVEL_WIDTH_X + DisplayConstants.BEVEL_WIDTH_Y + 2;
if (canHaveInputs(displayedGem)) {
gemWidth += DisplayConstants.INPUT_AREA_WIDTH;
}
if (hasOutput(displayedGem)) {
gemWidth += DisplayConstants.OUTPUT_AREA_WIDTH;
}
// Calculate height
int gemHeight = (Math.max(displayedGem.getGem().getNInputs(), 1) + 1) * DisplayConstants.ARGUMENT_SPACING;
return new Dimension(gemWidth, gemHeight);
}
/**
* {@inheritDoc}
*/
@Override
public Shape getBodyShape() {
Rectangle bodyBounds = getBodyBounds();
float centreY = (float)bodyBounds.getCenterY();
// The main Gem triangle
Polygon gemTri = new Polygon();
gemTri.addPoint(bodyBounds.x, bodyBounds.y);
gemTri.addPoint(bodyBounds.x + bodyBounds.width, (int) centreY);
gemTri.addPoint(bodyBounds.x, bodyBounds.y + bodyBounds.height);
return gemTri;
}
/**
* {@inheritDoc}
*/
@Override
public Point2D getCenterPoint() {
// Note that for a triangle, the centroid is simply the average of its three vertices..
// Thus, the weighted center is a third of the distance from the thicker side.
Rectangle bodyBounds = getBodyBounds();
double centreX = bodyBounds.getX() + (bodyBounds.getWidth() / 3.0);
return new Point2D.Double(centreX, bodyBounds.getCenterY());
}
}
/**
* A DisplayedGemShape representing an Rectangular shape.
* @author Edward Lam
*/
public static class Rectangular extends DisplayedGemShape {
/**
* Constructor for a Rectangular DisplayedGemShape.
* @param displayedGem
* @param innerComponentInfo
*/
Rectangular(DisplayedGem displayedGem, InnerComponentInfo innerComponentInfo) {
super(displayedGem, innerComponentInfo);
}
/**
* {@inheritDoc}
*/
@Override
public Point getInputConnectPoint(int inputIndex) {
if (inputIndex == 0 && canHaveInputs(displayedGem)) {
Rectangle inRect = getInBounds();
int centreY = (int)inRect.getCenterY();
return new Point(inRect.x, centreY);
} else {
return null;
}
}
/**
* {@inheritDoc}
*/
@Override
public Shape getBodyShape() {
Rectangle bodyBounds = getBodyBounds();
// The main Gem rectangle
Polygon gemRect = new Polygon();
gemRect.addPoint(bodyBounds.x, bodyBounds.y);
gemRect.addPoint(bodyBounds.x + bodyBounds.width, bodyBounds.y);
gemRect.addPoint(bodyBounds.x + bodyBounds.width, bodyBounds.y + bodyBounds.height);
gemRect.addPoint(bodyBounds.x, bodyBounds.y + bodyBounds.height);
return gemRect;
}
/**
* {@inheritDoc}
*/
@Override
public Dimension getDimensions() {
Rectangle innerComponentBounds = innerComponentInfo.getBounds();
// Calculate width
int gemWidth = innerComponentBounds.width + DisplayConstants.BEVEL_WIDTH_X * 2 + 1;
if (canHaveInputs(displayedGem)) {
gemWidth += DisplayConstants.INPUT_AREA_WIDTH;
}
if (hasOutput(displayedGem)) {
gemWidth += DisplayConstants.OUTPUT_AREA_WIDTH;
}
// Calculate height
int gemHeight = innerComponentBounds.height + DisplayConstants.BEVEL_WIDTH_Y * 2 + 1;
// Return the dimensions
return new Dimension(gemWidth, gemHeight);
}
}
/**
* Constructor for a DisplayedGemShape
* @param displayedGem the gem being displayed.
* @param innerComponentInfo the info on the component which should fit inside the shape of the gem.
*/
DisplayedGemShape(DisplayedGem displayedGem, InnerComponentInfo innerComponentInfo) {
if (displayedGem == null || innerComponentInfo == null) {
throw new NullPointerException("Arguments must not be null.");
}
this.displayedGem = displayedGem;
this.innerComponentInfo = innerComponentInfo;
}
/**
* Get information on the image which is supposed to fit inside the shape representing the displayed gem
* @return the inner component info..
*/
public InnerComponentInfo getInnerComponentInfo() {
return innerComponentInfo;
}
/**
* Determine the shape's dimensions.
* @return the dimensions of the gem.
*/
public abstract Dimension getDimensions();
/**
* Return a Shape representing this Gem's body.
* @return Shape the body shape
*/
public abstract Shape getBodyShape();
/**
* Get the Shape representing the body shape of this Gem, scaled by an appropriate amount
* @param scaleFactor the scaling factor of the shape.
* @return Shape the body shape, scaled by scaleFactor
*/
Shape getScaledBodyShape(double scaleFactor) {
Shape bodyShape = getBodyShape();
Point2D centroid = getCenterPoint();
AffineTransform scaleTransform = AffineTransform.getScaleInstance(scaleFactor, scaleFactor);
double transformedX = centroid.getX() / scaleFactor;
double transformedY = centroid.getY() / scaleFactor;
scaleTransform.translate(transformedX - centroid.getX(), transformedY - centroid.getY());
return scaleTransform.createTransformedShape(bodyShape);
}
/**
* Get a fresh, unscaled graphics object from which device-free (user-space) calculations can be performed.
* @return an unscaled graphics object.
*/
static Graphics getGraphics() {
return dummyImage.getGraphics();
}
/**
* Return a copy of the Gem's bounds.
* @return Rectangle the bounds of the Gem
*/
public final Rectangle getBounds() {
// If we don't already have have a cached bounds, calculate this now
if (cachedBounds == null) {
// The bounds are the combination of location and size
cachedBounds = new Rectangle(displayedGem.getLocation(), getDimensions());
}
return new Rectangle(cachedBounds);
}
/**
* Determine the Gem's input area bounds.
* Note that even if the gem currently has no input, if there exists a potential for input
* (eg. 0-input DataConstructors or CodeGems)
* @return Rectangle the bounds of the input area, or null if there cannot be any inputs.
*/
public Rectangle getInBounds() {
if (canHaveInputs(displayedGem)) {
// The bounds are the same as the full size, less a lot of it's width
Rectangle inBounds = getBounds();
inBounds.width = DisplayConstants.INPUT_AREA_WIDTH;
return inBounds;
} else {
return null;
}
}
/**
* Determine the Gem's output area bounds.
* @return Rectangle the bounds of the output area, or null if there is no output.
*/
public final Rectangle getOutBounds() {
// For now, just return the output area - should be finer grained!
if (hasOutput(displayedGem)) {
// The bounds are the same as the full size, less a lot of it's width
Rectangle outBounds = getBounds();
outBounds.x += outBounds.width - DisplayConstants.OUTPUT_AREA_WIDTH;
outBounds.width = DisplayConstants.OUTPUT_AREA_WIDTH;
return outBounds;
} else {
return null;
}
}
/**
* Determine the Gem's body area bounds.
* @return Rectangle the bounds of the body section
*/
public final Rectangle getBodyBounds() {
// If we don't already have have a cached bounds, calculate this now
if (cachedBodyBounds == null) {
// The bounds are the same as the full size, minus some of the width for connecting bits
cachedBodyBounds = new Rectangle(getBounds());
if (hasOutput(displayedGem)) {
cachedBodyBounds.width -= DisplayConstants.OUTPUT_AREA_WIDTH;
}
if (canHaveInputs(displayedGem)) {
cachedBodyBounds.x += DisplayConstants.INPUT_AREA_WIDTH;
cachedBodyBounds.width -= DisplayConstants.INPUT_AREA_WIDTH;
}
}
return new Rectangle(cachedBodyBounds);
}
/**
* Clear bound objects.
*/
void purgeCachedBounds() {
cachedBounds = null;
cachedBodyBounds = null;
}
/**
* 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 Point2D getCenterPoint() {
// by default, the point will be in the centre of the body bounds.
Rectangle bounds = getBodyBounds();
return new Point2D.Double(bounds.getCenterX(), bounds.getCenterY());
}
/**
* Get the point at which connects to the nth input are made
* @param inputIndex the argument number
* @return the input point
*/
public abstract Point getInputConnectPoint(int inputIndex);
/**
* Get the point at which connects to the output are made
* @return the output point
*/
public Point getOutputConnectPoint() {
// For now, the connection points for outputs can all be calculated in the same way.
Rectangle outRect = getOutBounds();
float centreY = (float) outRect.getCenterY();
return new Point(outRect.x + outRect.width, (int)centreY);
}
/**
* Return an Shape representing this Gem's nth input.
* @param bindPoint the location of the bind point
* @return Shape the shape of the bind point
*/
static Shape getInputShape(Point bindPoint) {
// Where does this blob go vertically
int x = bindPoint.x;
int y = bindPoint.y;
// Where does this blob go vertically
return new Ellipse2D.Double(x + DisplayConstants.BIND_POINT_RADIUS * 0.5, y - DisplayConstants.BIND_POINT_RADIUS, DisplayConstants.BIND_POINT_RADIUS * 2, DisplayConstants.BIND_POINT_RADIUS * 2);
}
/**
* Return a polygon (arrow) representing this Gem's output.
* @return Polygon the output polygon
*/
Polygon getOutputShape() {
Point connectPoint = getOutputConnectPoint();
int x = connectPoint.x;
int y = connectPoint.y;
Polygon arrow = new Polygon();
arrow.addPoint(x - DisplayConstants.OUT_ARROW_SIZE, y - DisplayConstants.OUT_ARROW_SIZE);
arrow.addPoint(x, y);
arrow.addPoint(x - DisplayConstants.OUT_ARROW_SIZE, y + DisplayConstants.OUT_ARROW_SIZE);
return arrow;
}
/**
* Return a dithered (larger) shape representing this Gem's output.
* Use this to determine whether a mouse click is 'close' enough.
* @return Polygon the output polygon
*/
private Shape getDitheredOutputShape() {
if (hasOutput(displayedGem)) {
Point connectPoint = getOutputConnectPoint();
int x = connectPoint.x;
int y = connectPoint.y;
int arrowSize = DisplayConstants.OUT_ARROW_SIZE + DisplayConstants.DITHER_FACTOR;
Polygon arrow = new Polygon();
arrow.addPoint(x + (int)(DisplayConstants.DITHER_FACTOR * 0.75) - arrowSize, y - arrowSize);
arrow.addPoint(x + (int)(DisplayConstants.DITHER_FACTOR * 0.75), y);
arrow.addPoint(x + (int)(DisplayConstants.DITHER_FACTOR * 0.75) - arrowSize, y + arrowSize);
return arrow;
} else {
return null;
}
}
/**
* Return whether a given gem can have any inputs.
* Note that this is different from whether a given gem actually does have any inputs.
* eg. a code gem may or may not have inputs, but calling this method on that code gem will still always true.
* @param displayedGem the gem in question.
* @return whether the given gem can have any inputs.
*/
private static boolean canHaveInputs(DisplayedGem displayedGem) {
return !(displayedGem.getGem() instanceof ValueGem);
}
/**
* Return whether a given gem has an output part.
* @param displayedGem the gem in question.
* @return whether the given gem has an output.
*/
private static boolean hasOutput(DisplayedGem displayedGem) {
return !(displayedGem.getGem() instanceof CollectorGem);
}
/**
* Determine if the body is under the given point.
* @param xy Point the point to test for
* @return boolean whether we hit
*/
public final boolean bodyHit(Point xy) {
// Does this hit the body
return getBodyShape().contains(xy);
}
/**
* Determine if the output blob is under the given point.
* @param xy Point the point to test for
* @return boolean whether we hit
*/
public final boolean outputHit(Point xy){
if (hasOutput(displayedGem)) {
// Test the blob, allow for 'close' hits.
return getDitheredOutputShape().contains(xy);
} else {
return false;
}
}
/**
* Determine if (which) input blob under the given point.
* @param xy Point the point to test for
* @return int which input we hit (-1 for none)
*/
public final int inputHit(Point xy) {
// Some useful constants..
// The input circle is drawn with the given radius. We allow clickability within a certain dither amount from the circle.
double radialDither = DisplayConstants.DITHER_FACTOR/2;
double ditheredRadius = DisplayConstants.BIND_POINT_RADIUS + radialDither;
if (canHaveInputs(displayedGem)) {
// Iterate over the inputs.
int nInputs = displayedGem.getNDisplayedArguments();
for (int i = 0; i < nInputs; i++) {
// Get the connect point.
Point connectPoint = getInputConnectPoint(i);
// test for distance..
double distance = Math.sqrt(Math.pow(xy.getX() - connectPoint.getX(), 2) + Math.pow(xy.getY() - connectPoint.getY(), 2));
if (distance <= ditheredRadius) {
return i;
}
}
}
return -1;
}
/**
* Determine if any input name tag is under the given point
* @return int index of the input which we hit, -1 for none(no name tags at all or none were clicked)
*/
public final int inputNameTagHit(Point xy){
List<Rectangle> nameTagBounds = innerComponentInfo.getInputNameLabelBounds();
if (nameTagBounds != null){
for (int i = 0, nRects = nameTagBounds.size(); i < nRects ; i ++) {
Rectangle nameTagBound = nameTagBounds.get(i);
if (nameTagBound.contains(xy)) {
return i;
}
}
}
return -1;
}
}