// Charles A. Loomis, Jr., and University of California, Santa Cruz,
// Copyright (c) 2000
package org.freehep.swing.graphics;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Component;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.Stroke;
import java.awt.geom.AffineTransform;
import java.awt.geom.GeneralPath;
import java.awt.geom.Point2D;
import java.awt.geom.Point2D.Double;
import org.freehep.graphics2d.VectorGraphics;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.border.Border;
/**
* This class implements a Border in which the left and bottom sides
* contain numerical scales.
*
* @author Charles Loomis
* @version $Id: ScaleBorder.java 8584 2006-08-10 23:06:37Z duns $ */
public class ScaleBorder
implements Border{
/**
* Constant giving the unicode value of a capital Greek delta. */
final static private String DELTA = "\u0394";
/**
* Constant giving the unicode value of a prime symbol. */
final static private String PRIME = "\u00b4";
/**
* Constant giving the unicode value of a middle dot. */
final static private String DOT = "\u00b7";
/**
* A constant giving the horizontal index of the data arrays. */
final static private int HORIZONTAL = 0;
/**
* A constant giving the vertical index of the data arrays. */
final static private int VERTICAL = 1;
/**
* Constant describing a transform in which the transformed x and
* y axes are parallel (or antiparallel) to the original axes. */
final static public int TYPE_PARALLEL_TRANSFORM = 0;
/**
* Constant describing a transform in which the transformed x and
* y axes are parallel (or antiparallel) to the original y and x
* axes, respectively. */
final static public int TYPE_SWITCHED_TRANSFORM = 1;
/**
* Constant describing a transform in which the transformed x-axis
* is parallel (or antiparallel) to the original x-axis and the
* transformed y-axis forms a non-zero angle with the original
* one. */
final static public int TYPE_X_SKEW_TRANSFORM = 2;
/**
* Constant describing a transform in which the transformed y-axis
* is parallel (or antiparallel) to the original y-axis and the
* transformed x-axis forms a non-zero angle with the original
* one. */
final static public int TYPE_Y_SKEW_TRANSFORM = 3;
/**
* Constant describing a transform in which the transformed x-axis
* is parallel (or antiparallel) to the original y-axis and the
* transformed y-axis forms a non-zero angle with the original
* x-axis. */
final static public int TYPE_SWITCHED_X_SKEW_TRANSFORM = 4;
/**
* Constant describing a transform in which the transformed y-axis
* is parallel (or antiparallel) to the original x-axis and the
* transformed x-axis forms a non-zero angle with the original
* y-axis. */
final static public int TYPE_SWITCHED_Y_SKEW_TRANSFORM = 5;
/**
* Constant describing a transform which does not fall into one of
* the other categories. */
final static public int TYPE_GENERAL_TRANSFORM = 6;
/**
* Background color. */
private Color bkgColor;
/**
* Foreground color. */
private Color fgColor;
/**
* Paths describing the primary tick marks for the two axes. */
private GeneralPath[] pTicks = new GeneralPath[2];
/**
* Paths describing the secondary tick marks for the two axes. */
private GeneralPath[] sTicks = new GeneralPath[2];
/**
* String giving the axis labels. */
private String[] axisLabels = new String[2];
/**
* String giving the axis units. */
private String[] axisUnits = new String[2];
/**
* The labels for the horizontal and vertical axes. */
private String[][] labels;
/**
* The positions of each of the axis labels. */
private double[][] positions;
/**
* The initial font size for labeling. */
private int fontSize = 12;
/**
* The initial font to use. */
private Font labelFont = new Font("SansSerif", Font.BOLD, fontSize);
/**
* The width of the line used for the secondary tick marks. */
final static private Stroke thinStroke = new BasicStroke(1.f);
/**
* The width of the line used for the primary tick marks. */
final static private Stroke thickStroke = new BasicStroke(2.f);
/**
* An array to hold temporary point values for transformation. */
private double[] axisPts = new double[6];
/**
* Flag to indicate that the scale has changed and that it needs
* to be redrawn. */
private boolean scaleChanged;
/**
* The current width of the horizontal scale. */
private int currentWidth;
/**
* The current width of the vertical scale. */
private int currentHeight;
/**
* Minimum value on the horizontal axis. */
private double minHoriz;
/**
* Maximum value of the horizontal axis. */
private double maxHoriz;
/**
* Minimum value of the vertical axis. */
private double minVert;
/**
* Maximum value on the vertical axis. */
private double maxVert;
/**
* The calculated Insets for this border. */
private Insets insets;
/**
* Constructs a ScaleBorder with the default foreground (black) and
* background (orange) colors. */
public ScaleBorder() {
this(Color.orange,Color.black);
}
/**
* Constructs a ScaleBorder with the given background and
* foreground colors. */
public ScaleBorder(Color bkgColor, Color fgColor) {
// Give default values for the size of the axes.
minHoriz = 0.;
maxHoriz = 0.;
minVert = 0.;
maxVert = 0.;
// Make the new insets.
resetInsets();
// Make the paths which will hold the axes.
pTicks[HORIZONTAL] = new GeneralPath();
pTicks[VERTICAL] = new GeneralPath();
sTicks[HORIZONTAL] = new GeneralPath();
sTicks[VERTICAL] = new GeneralPath();
// Make the labels and positions.
labels = new String[2][3];
positions = new double[2][3];
// Set the current width and height.
currentWidth = 0;
currentHeight = 0;
scaleChanged = false;
// Set the colors.
setBackgroundColor(bkgColor);
setForegroundColor(fgColor);
}
/**
* Set the background color. */
public void setBackgroundColor(Color bkgColor) {
this.bkgColor = bkgColor;
}
/**
* Get the current background color. */
public Color getBackgroundColor() {
return bkgColor;
}
/**
* Set the foreground color. */
public void setForegroundColor(Color bkgColor) {
this.fgColor = bkgColor;
}
/**
* Get the current foreground color. */
public Color getForegroundColor() {
return fgColor;
}
/**
* Set the font for the labels. */
public void setLabelFont(Font labelFont) {
this.labelFont = labelFont;
fontSize = labelFont.getSize();
resetInsets();
}
/**
* Get the current font for the labels. */
public Font getLabelFont() {
return labelFont;
}
/**
* Set the horizontal and vertical limits for the scales. */
public void setLimits(double minHoriz, double maxHoriz,
double minVert, double maxVert) {
this.minHoriz = minHoriz;
this.maxHoriz = maxHoriz;
this.minVert = minVert;
this.maxVert = maxVert;
scaleChanged = true;
}
/**
* Set the axis labels. */
public void setAxisLabels(String horizontalLabel,
String verticalLabel) {
axisLabels[HORIZONTAL] = horizontalLabel;
axisLabels[VERTICAL] = verticalLabel;
}
/**
* Set the axis units. */
public void setAxisUnits(String horizontalUnits,
String verticalUnits) {
if (horizontalUnits!=null) {
axisUnits[HORIZONTAL] = "("+horizontalUnits+")";
} else {
axisUnits[HORIZONTAL] = "";
}
if (verticalUnits!=null) {
axisUnits[VERTICAL] = "("+verticalUnits+")";
} else {
axisUnits[VERTICAL] = "";
}
}
/**
* Returns the insets of this border. */
public Insets getBorderInsets(Component c) {
return (Insets) insets.clone();
}
/**
* Returns whether or not the border is opaque. This always
* returns true. */
public boolean isBorderOpaque() {
return true;
}
/**
* Paints the border for the specified component with the
* specified graphics context, position, and size. */
public void paintBorder(Component c, Graphics g,
int x, int y,
int width, int height) {
// Set the flag indicating that the scales must be remade.
scaleChanged =
(scaleChanged || width!=currentWidth || height!=currentHeight);
VectorGraphics vg =
VectorGraphics.create(g);
if (vg!=null) {
// First fill in the background.
vg.setColor(bkgColor);
vg.fillRect(0,0,width,insets.top);
vg.fillRect(0,0,insets.left,height);
vg.fillRect(width-insets.right,0,insets.right,height);
vg.fillRect(0,height-insets.bottom,width,insets.bottom);
// Get the scale.
if (scaleChanged) {
Scale.drawLinearScale(minHoriz,maxHoriz,
width-(insets.left+insets.right),3,7,
Scale.RIGHT_TICKS,
pTicks[HORIZONTAL],
sTicks[HORIZONTAL],
labels[HORIZONTAL],
positions[HORIZONTAL]);
}
// Create a sub-graphics context to isolate the coordinate
// transformations.
Graphics sg = vg.create();
VectorGraphics svg =
VectorGraphics.create(sg);
svg.translate(insets.left,height-insets.bottom+2);
// Set the color and font.
svg.setColor(fgColor);
svg.setFont(labelFont);
// Draw the primary and secondary tick marks.
svg.setStroke(thickStroke);
svg.draw(pTicks[HORIZONTAL]);
svg.setStroke(thinStroke);
svg.draw(sTicks[HORIZONTAL]);
// Paint the tick labels.
for (int i=0; i<3; i++) {
if (labels[HORIZONTAL][i]!=null) {
svg.drawString(labels[HORIZONTAL][i],
(float) positions[HORIZONTAL][i],
Scale.getPrimaryTickSize()+0.2f*fontSize,
VectorGraphics.TEXT_CENTER,
VectorGraphics.TEXT_TOP);
}
}
// Paint the axis label. The third position is always set
// so that the axis label should be between this position
// and one of the other two.
float axisPosition;
if (Math.abs(positions[HORIZONTAL][2]-positions[HORIZONTAL][1])>
Math.abs(positions[HORIZONTAL][2]-positions[HORIZONTAL][0])) {
axisPosition = (float) (0.5*(positions[HORIZONTAL][2]+
positions[HORIZONTAL][1]));
} else {
axisPosition = (float) (0.5*(positions[HORIZONTAL][2]+
positions[HORIZONTAL][0]));
}
svg.drawString(axisLabels[HORIZONTAL]+" "+
axisUnits[HORIZONTAL],
axisPosition,
Scale.getPrimaryTickSize()+0.2f*fontSize,
VectorGraphics.TEXT_CENTER,
VectorGraphics.TEXT_TOP);
// End this context.
svg.dispose();
// Create the vertical scale.
if (scaleChanged) {
Scale.drawLinearScale(minVert,maxVert,
height-(insets.top+insets.bottom),3,7,
Scale.LEFT_TICKS,
pTicks[VERTICAL],
sTicks[VERTICAL],
labels[VERTICAL],
positions[VERTICAL]);
}
// Now create a new graphics context for the vertical axis.
sg = vg.create();
svg =
VectorGraphics.create(sg);
svg.translate(insets.left-2,height-insets.bottom);
svg.rotate(-Math.PI/2.);
// Set the color and font.
svg.setColor(fgColor);
svg.setFont(labelFont);
// Draw the primary and secondary tick marks.
svg.setStroke(thickStroke);
svg.draw(pTicks[VERTICAL]);
svg.setStroke(thinStroke);
svg.draw(sTicks[VERTICAL]);
// Paint the tick labels.
for (int i=0; i<3; i++) {
if (labels[VERTICAL][i]!=null) {
svg.drawString(labels[VERTICAL][i],
(float) positions[VERTICAL][i],
-(Scale.getPrimaryTickSize()+0.2f*fontSize),
VectorGraphics.TEXT_CENTER,
VectorGraphics.TEXT_BOTTOM);
}
}
// Paint the axis label. The third position is always set
// so that the axis label should be between this position
// and one of the other two.
if (Math.abs(positions[VERTICAL][2]-positions[VERTICAL][1])>
Math.abs(positions[VERTICAL][2]-positions[VERTICAL][0])) {
axisPosition = (float) (0.5*(positions[VERTICAL][2]+
positions[VERTICAL][1]));
} else {
axisPosition = (float) (0.5*(positions[VERTICAL][2]+
positions[VERTICAL][0]));
}
svg.drawString(axisLabels[VERTICAL]+" "+
axisUnits[VERTICAL],
axisPosition,
-(Scale.getPrimaryTickSize()+0.2f*fontSize),
VectorGraphics.TEXT_CENTER,
VectorGraphics.TEXT_BOTTOM);
// End this context.
svg.dispose();
// Reset the current width and height.
currentWidth = width;
currentHeight = height;
scaleChanged = false;
}
}
/**
* Recalculate the insets based on the current font size. */
private void resetInsets() {
int lb = (int) (2+2+Scale.getPrimaryTickSize()+1.5*fontSize);
int tr = (int) (2+2+Scale.getPrimaryTickSize());
insets = new Insets(tr,lb,lb,tr);
}
/**
* Set the scale labels taking into account the given linear
* transformation. */
public void setScales(String horizLabel,
String vertLabel,
String horizUnits,
String vertUnits,
AffineTransform transform,
int panelWidth,
int panelHeight) {
// Determine the type of the tranform.
int type = classifyTransform(transform);
switch (type) {
case (TYPE_PARALLEL_TRANSFORM):
setAxisLabels(horizLabel,vertLabel);
setAxisUnits(horizUnits,vertUnits);
break;
case (TYPE_SWITCHED_TRANSFORM):
setAxisLabels(vertLabel,horizLabel);
setAxisUnits(vertUnits,horizUnits);
break;
case (TYPE_Y_SKEW_TRANSFORM):
setAxisLabels(horizLabel+PRIME,DELTA+vertLabel);
if (horizUnits.equals(vertUnits)) {
setAxisUnits(horizUnits,vertUnits);
} else {
setAxisUnits(horizUnits+DOT+vertUnits,vertUnits);
}
break;
case (TYPE_X_SKEW_TRANSFORM):
setAxisLabels(DELTA+horizLabel,vertLabel+PRIME);
if (vertUnits.equals(horizUnits)) {
setAxisUnits(horizUnits,vertUnits);
} else {
setAxisUnits(horizUnits,vertUnits+DOT+horizUnits);
}
break;
case (TYPE_SWITCHED_Y_SKEW_TRANSFORM):
setAxisLabels(vertLabel+PRIME,DELTA+horizLabel);
if (vertUnits.equals(horizUnits)) {
setAxisUnits(vertUnits,horizUnits);
} else {
setAxisUnits(vertUnits+DOT+horizUnits,horizUnits);
}
break;
case (TYPE_SWITCHED_X_SKEW_TRANSFORM):
setAxisLabels(DELTA+vertLabel,horizLabel+PRIME);
if (horizUnits.equals(vertUnits)) {
setAxisUnits(vertUnits,horizUnits);
} else {
setAxisUnits(vertUnits,horizUnits+DOT+vertUnits);
}
break;
default:
setAxisLabels(horizLabel+PRIME,vertLabel+PRIME);
if (horizUnits.equals(vertUnits)) {
setAxisUnits(horizUnits,vertUnits);
} else {
setAxisUnits(horizUnits+DOT+vertUnits,
vertUnits+DOT+horizUnits);
}
break;
}
// Get the size of the scales.
axisPts[0] = 0.;
axisPts[1] = panelHeight;
axisPts[2] = 0.;
axisPts[3] = panelHeight;
axisPts[4] = panelWidth;
axisPts[5] = 0.;
try {
// Avoid using the following call because of a bug in
// AffineTransform. Instead create the inverse matrix
// explicitly as done below.
//transform.inverseTransform(physicsPt,0,physicsPt,0,3);
AffineTransform ixform = transform.createInverse();
ixform.transform(axisPts,0,axisPts,0,1);
ixform.deltaTransform(axisPts,2,axisPts,2,2);
// Calculate the values for the vertical axis and the
// distance.
double vdy = axisPts[3];
double vdx = axisPts[2];
double vdist = Math.sqrt(vdx*vdx+vdy*vdy);
// Calculate the values for the horizontal axis and the
// distance.
double hdy = axisPts[5];
double hdx = axisPts[4];
double hdist = Math.sqrt(hdx*hdx+hdy*hdy);
// Initialize the endpoints of the axes.
double vmin = 0.;
double vmax = 0.;
double hmin = 0.;
double hmax = 0.;
// Do what is necessary for the different types of
// transformations.
switch (type) {
case (TYPE_PARALLEL_TRANSFORM): {
double vsign = (vdy<0.) ? 1. : -1.;
vmin = axisPts[1];
vmax = axisPts[1]+vsign*vdist;
double hsign = (hdx>0.) ? 1. : -1.;
hmin = axisPts[0];
hmax = axisPts[0]+hsign*hdist;
break;
}
case (TYPE_SWITCHED_TRANSFORM): {
double hsign = (hdy>0.) ? 1. : -1.;
hmin = axisPts[1];
hmax = axisPts[1]+hsign*hdist;
double vsign = (vdx<0.) ? 1. : -1.;
vmin = axisPts[0];
vmax = axisPts[0]+vsign*vdist;
break;
}
case (TYPE_Y_SKEW_TRANSFORM): {
double vsign = (vdy>0.) ? 1. : -1.;
vmax = -vsign*vdist/2.;
vmin = -vmax;
hmin = 0.;
hmax = hdist;
break;
}
case (TYPE_X_SKEW_TRANSFORM): {
double hsign = (hdx>0.) ? 1. : -1.;
hmax = hsign*hdist/2.;
hmin = -hmax;
vmin = 0.;
vmax = vdist;
break;
}
case (TYPE_SWITCHED_Y_SKEW_TRANSFORM): {
double vsign = (vdx>0.) ? 1. : -1.;
vmax = -vsign*vdist/2.;
vmin = -vmax;
hmin = 0.;
hmax = hdist;
break;
}
case (TYPE_SWITCHED_X_SKEW_TRANSFORM): {
double hsign = (hdy>0.) ? 1. : -1.;
hmax = hsign*hdist/2.;
hmin = -hmax;
vmin = 0.;
vmax = vdist;
break;
}
default: {
vmin = 0.;
vmax = vdist;
hmin = 0.;
hmax = hdist;
break;
}
}
// Actually set the limits.
setLimits(hmin,hmax,vmin,vmax);
} catch (Exception e) {
setLimits(0.,0.,0.,0.);
}
}
/**
* This is a protected utility method which classifies the given
* transform into seven categories: parallel, switched, x-skew,
* y-skew, switched x-skew, switched y-skew, and general. The
* parallel category describes transformations in which the
* transformed x and y axes are parallel or antiparallel to the
* original x and y axes, respectively. The switched category
* describes transformations in which the transformed x and y axes
* are parallel or antiparallel to the original y and x axes,
* respectively. That is, the x and y axes have been switched.
* The x-skew describes transformations in which the transformed
* x-axis is parallel (or antiparallel) to the original one while
* the transformed y-axis forms some non-zero angle to the
* original one. The y-skew is similar; the switch skews are just
* rotated (counter)clockwise by 90 degrees. The general category
* encompasses all transforms not falling into one of the other
* categories. */
static protected int classifyTransform(AffineTransform xform) {
// Set the default return type to a general matrix.
int category = TYPE_GENERAL_TRANSFORM;
// Get the four non-translation quantities from the
// transformation.
double sx = xform.getScaleX();
double sy = xform.getScaleY();
double kx = xform.getShearX();
double ky = xform.getShearY();
// Check the type.
if (kx==0. && ky==0.) {
category = TYPE_PARALLEL_TRANSFORM;
} else if (sx==0. && sy==0.) {
category = TYPE_SWITCHED_TRANSFORM;
} else if (kx==0.) {
category = TYPE_Y_SKEW_TRANSFORM;
} else if (ky==0.) {
category = TYPE_X_SKEW_TRANSFORM;
} else if (sx==0.) {
category = TYPE_SWITCHED_Y_SKEW_TRANSFORM;
} else if (sy==0.) {
category = TYPE_SWITCHED_X_SKEW_TRANSFORM;
}
// Return the transformtion type.
return category;
}
}