/*
* Copyright 2004-2010 Information & Software Engineering Group (188/1)
* Institute of Software Technology and Interactive Systems
* Vienna University of Technology, Austria
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.ifs.tuwien.ac.at/dm/somtoolbox/license.html
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package at.tuwien.ifs.somtoolbox.apps.viewer;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Polygon;
import java.awt.geom.Line2D;
import edu.umd.cs.piccolo.PNode;
import edu.umd.cs.piccolo.util.PPaintContext;
import at.tuwien.ifs.somtoolbox.data.SOMVisualisationData;
import at.tuwien.ifs.somtoolbox.input.InputCorrections;
import at.tuwien.ifs.somtoolbox.input.InputCorrections.CreationType;
import at.tuwien.ifs.somtoolbox.input.InputCorrections.InputCorrection;
import at.tuwien.ifs.somtoolbox.visualization.comparison.Shift;
/**
* Class for arrows on the SOM Visualization (that indicate shifts in a SOM)
*
* @author Doris Baum
* @version $Id: ArrowPNode.java 3589 2010-05-21 10:42:01Z mayer $
*/
public class ArrowPNode extends PNode {
private static final long serialVersionUID = 1L;
// coordinates of the arrow
private double x1 = 0;
private double y1 = 0;
private double x2 = 0;
private double y2 = 0;
// line object for arrow
private Line2D.Double arrow = null;
private boolean typeVisibility = true;
private boolean selectionVisibility = true;
// do we want to show the arrowheads at the beginning and end of the line?
private boolean showStartHead = false;
private boolean showEndHead = true;
private PNode tooltipNode;
// how long and broad should the arrowheads be?
private double arrowHeadLength = 40;
private double arrowHeadWidth = 20;
// polygon object for drawing the arrowheads
private Polygon startHead = null;
private Polygon endHead = null;
// arrow color
private Color color = Color.orange;
private static final Color OUTLIERCOLOR = Color.red;
private static final Color STABLECOLOR = Color.green;
private static final Color ADJACENTCOLOR = Color.cyan;
private static final Color CLUSTERCOLOR = Color.blue;
// stroke for the line
private BasicStroke stroke = new BasicStroke(2.0f);
// type of arrow: outlier or stable data vector? determines color
private int type = -1;
private CreationType creationType;
private float arrowHeadScale = 1f;
public static final int OUTLIER = Shift.OUTLIER;
public static final int STABLE = Shift.STABLE;
public static final int ADJACENT = Shift.ADJACENT;
public static final int CLUSTER = Shift.CLUSTER;
public static final int MAXARROWWIDTH = 20;
public ArrowPNode(Point start, Point end, double offsetX, double offsetY) {
this(start.x + offsetX, start.y + offsetY, end.x + offsetX, end.y + offsetY);
}
/**
* Constructor that takes arrow coordinates
*/
public ArrowPNode(double x1, double y1, double x2, double y2) {
super();
this.setPaint(color);
arrow = new Line2D.Double();
startHead = new Polygon();
endHead = new Polygon();
this.setArrow(x1, y1, x2, y2);
}
public ArrowPNode(InputCorrections.CreationType creationType, String[][] attributes, String tooltip, Color color,
double width, double height, Point pointBegin, Point pointEnd) {
this(pointBegin, pointEnd, width / 2, height / 2);
this.creationType = creationType;
for (String[] attribute : attributes) {
addAttribute(attribute[0], attribute[1]);
}
tooltipNode = new PNode();
tooltipNode.addAttribute("tooltip", tooltip);
addChild(tooltipNode);
tooltipNode.moveToFront();
setColor(color);
}
/**
* Show the arrowhead at the (x1,y1)-end of the line?
*
* @param state arrowhead "on" or "off"?
*/
public void showStartArrowHead(boolean state) {
this.showStartHead = state;
}
public void setArrowHeadScale(float scale) {
this.arrowHeadScale = scale;
}
/**
* Show the arrowhead at the (x2,y2)-end of the line?
*
* @param state arrowhead "on" or "off"?
*/
public void showEndArrowHead(boolean state) {
this.showEndHead = state;
}
/**
* Calculate the coordinates for the 3 points defining each arrowhead (the arrowhead is a triangle) and generate
* polygon objects with these points. The polygon objects are then held in attributes startHead and endHead. The
* lenght of the arrow line is adjusted so that it doesn't overlap with the arrowheds
*/
private void calculateArrow() {
// Black Magic happens here... (it all makes sense if you draw
// little sketches of arrowheads. I promise.)
double width = Math.abs(x2 - x1);
double height = Math.abs(y2 - y1);
double alpha = Math.atan(width / height);
double deltax1 = arrowHeadScale * arrowHeadLength * Math.sin(alpha);
double deltay1 = arrowHeadScale * arrowHeadLength * Math.cos(alpha);
double deltax2 = arrowHeadScale * arrowHeadWidth / 2 * Math.cos(alpha);
double deltay2 = arrowHeadScale * arrowHeadWidth / 2 * Math.sin(alpha);
double signA = 1;
double signB = 1;
if (x1 < x2) {
signA = 1;
} else {
signA = -1;
}
if (y1 < y2) {
signB = 1;
} else {
signB = -1;
}
startHead = new Polygon();
startHead.addPoint((int) x1, (int) y1);
startHead.addPoint((int) (x1 + deltax1 * signA + deltax2 * signB), (int) (y1 + deltay1 * signB - deltay2
* signA));
startHead.addPoint((int) (x1 + deltax1 * signA - deltax2 * signB), (int) (y1 + deltay1 * signB + deltay2
* signA));
endHead = new Polygon();
endHead.addPoint((int) x2, (int) y2);
endHead.addPoint((int) (x2 - deltax1 * signA + deltax2 * signB), (int) (y2 - deltay1 * signB - deltay2 * signA));
endHead.addPoint((int) (x2 - deltax1 * signA - deltax2 * signB), (int) (y2 - deltay1 * signB + deltay2 * signA));
double x1adj = x1;
double y1adj = y1;
double x2adj = x2;
double y2adj = y2;
if (showStartHead) {
x1adj = x1 + deltax1 * signA;
y1adj = y1 + deltay1 * signB;
}
if (showEndHead) {
x2adj = x2 - deltax1 * signA;
y2adj = y2 - deltay1 * signB;
}
arrow.setLine(x1adj, y1adj, x2adj, y2adj);
if (tooltipNode != null) {
tooltipNode.setBounds(endHead.getBounds2D());
}
}
/**
* (Re)sets the coordinates of the arrow to (x1, y1) (x2, y2); produces a line and appropriate arrowheads and resets
* the Node's bounding box to hold the new arrow
*/
public void setArrow(double x1, double y1, double x2, double y2) {
// save coordinates
this.x1 = x1;
this.y1 = y1;
this.x2 = x2;
this.y2 = y2;
// calculate bounding box:
// width and height
double width = Math.abs(x1 - x2);
double height = Math.abs(y1 - y2);
// if this is a horizontal or vertical line, make sure the width or hight isn't 0
// so that the bounding box doesn't disappear -- this would lead to the line not being drawn
if (width == 0) {
width = 1;
}
if (height == 0) {
height = 1;
}
double boundX = 0;
double boundY = 0;
// and bounding box coordinates
if (x1 < x2) {
boundX = x1;
} else {
boundX = x2;
}
if (y1 < y2) {
boundY = y1;
} else {
boundY = y2;
}
this.setBounds(boundX, boundY, width, height);
}
/**
* Set the bounds of this PNode object.
*
* @see edu.umd.cs.piccolo.PNode#setBounds(double, double, double, double)
*/
@Override
public boolean setBounds(double x, double y, double w, double h) {
return super.setBounds(x, y, w, h);
}
/**
* Paints the arrow line with given color and stroke, adding arrowheads as applicable
*
* @see edu.umd.cs.piccolo.PNode#paint(edu.umd.cs.piccolo.util.PPaintContext)
*/
@Override
protected void paint(PPaintContext paintContext) {
calculateArrow();
Graphics2D g2d = paintContext.getGraphics();
g2d.setPaint(color);
g2d.setStroke(stroke);
g2d.draw(arrow);
if (showStartHead) {
g2d.fill(startHead);
}
if (showEndHead) {
g2d.fill(endHead);
}
}
/**
* Is the visibility of this ArrowPNode currently set to true?
*/
@Override
public boolean getVisible() {
return this.typeVisibility && this.selectionVisibility;
}
public int getType() {
return type;
}
public CreationType getCreationType() {
return creationType;
}
public void setType(int type) {
this.type = type;
if (type == OUTLIER) {
color = OUTLIERCOLOR;
} else if (type == STABLE) {
color = STABLECOLOR;
} else if (type == CLUSTER) {
color = CLUSTERCOLOR;
} else if (type == ADJACENT) {
color = ADJACENTCOLOR;
} else {
this.type = -1;
}
}
public void setLineWidth(double width) {
arrowHeadLength = Math.sqrt(width) * 30;
arrowHeadWidth = Math.sqrt(width) * 15;
float w = (float) width;
stroke = new BasicStroke(w);
}
public void setProportionalWidth(double propWidth) {
double width = propWidth * MAXARROWWIDTH;
this.setLineWidth(width);
}
public void setSelectionVisibility(boolean selectionVisibility) {
this.selectionVisibility = selectionVisibility;
}
public void setTypeVisibility(boolean typeVisibility) {
this.typeVisibility = typeVisibility;
}
@Override
public void setVisible(boolean visibility) {
}
public void setColor(Color color) {
this.color = color;
}
public static ArrowPNode createInputCorrectionArrow(InputCorrection c, InputCorrections.CreationType creationType,
GeneralUnitPNode sourceUnitNode, GeneralUnitPNode targetUnitNode) {
String tooltipBegin;
Color colour;
if (creationType == InputCorrections.CreationType.MANUAL) {
tooltipBegin = "Manual correction";
colour = Color.RED;
} else {
tooltipBegin = "Computed correction";
colour = Color.GREEN;
}
String[][] attributes = new String[][] { { SOMVisualisationData.INPUT_CORRECTIONS, c.getLabel() } };
return new ArrowPNode(creationType, attributes, tooltipBegin + " '" + c.getLabel() + "' "
+ c.getSourceUnit().printCoordinates() + " -> " + c.getTargetUnit().printCoordinates(), colour,
sourceUnitNode.getWidth(), sourceUnitNode.getHeight(), sourceUnitNode.getPostion(),
targetUnitNode.getPostion());
}
}