/*
* Open Source Physics software is free software as described near the bottom of this code file.
*
* For additional information and documentation on Open Source Physics please see:
* <http://www.opensourcephysics.org/>
*/
package org.opensourcephysics.display;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsDevice;
import java.awt.KeyboardFocusManager;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentEvent;
import java.awt.event.MouseEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JViewport;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;
import javax.swing.event.MouseInputAdapter;
import javax.swing.text.JTextComponent;
import org.opensourcephysics.controls.OSPLog;
import org.opensourcephysics.controls.XML;
import org.opensourcephysics.controls.XMLControl;
import org.opensourcephysics.display.axes.CoordinateStringBuilder;
import org.opensourcephysics.display.dialogs.DrawingPanelInspector;
import org.opensourcephysics.display.dialogs.ScaleInspector;
import org.opensourcephysics.display.dialogs.XMLDrawingPanelInspector;
import org.opensourcephysics.tools.FontSizer;
import org.opensourcephysics.tools.ToolsRes;
import org.opensourcephysics.tools.VideoTool;
/**
* DrawingPanel renders drawable objects on its canvas.
* DrawingPanel provides drawable objects with methods that transform from world
* coordinates to pixel coordinates. World coordinates are defined by xmin, xmax,
* ymin, and ymax. These values are recalculated on-the-fly from preferred
* values if the aspect ratio is unity; otherwise, preferred values are used.
*
* If xmax>xmin then the coordinate scale increases from right to left.
* If xmax<xmin then the coordinate scale increases from left to right.
* If ymax>ymin then the coordinate scale increases from bottom to top.
* If ymax<ymin then the coordinate scale increases from top to bottom.
*
* @author Wolfgang Christian
* @author Joshua Gould
* @version 1.0
*/
public class DrawingPanel extends JPanel implements ActionListener, Renderable {
protected static final boolean RECORD_PAINT_TIMES = false; // set true to test painting time
protected long currentTime = System.currentTimeMillis();
/** Message box location */
public static final int BOTTOM_LEFT = 0;
/** Message box location */
public static final int BOTTOM_RIGHT = 1;
/** Message box location */
public static final int TOP_RIGHT = 2;
/** Message box location */
public static final int TOP_LEFT = 3;
protected JPopupMenu popupmenu = new JPopupMenu(); // right mouse click popup menu
protected JMenuItem propertiesItem, autoscaleItem, scaleItem, zoomInItem, zoomOutItem, snapshotItem; // the menu item for the properites dialog box
protected int leftGutter = 0, topGutter = 0, rightGutter = 0, bottomGutter = 0;
protected int leftGutterPreferred = 0, topGutterPreferred = 0, rightGutterPreferred = 0, bottomGutterPreferred = 0;
protected boolean clipAtGutter = true; // clips the drawing at the gutter if true
protected boolean adjustableGutter = false; // adjust gutter depending on panel size
protected int width, height; // the size of the panel the last time it was painted.
protected Color bgColor = new Color(239, 239, 255); // background color
protected boolean antialiasTextOn = false;
protected boolean antialiasShapeOn = false;
protected boolean squareAspect = false; // adjust xAspect and yAspect so the drawing aspect ratio is unity
protected boolean autoscaleX = true;
protected boolean autoscaleY = true;
protected boolean autoscaleXMin = true, autoscaleXMax = true;
protected boolean autoscaleYMin = true, autoscaleYMax = true;
protected double autoscaleMargin = 0.0; // used to increase the autoscale range
// x and y scale in world units
protected double xminPreferred = -10.0, xmaxPreferred = 10.0;
protected double yminPreferred = -10.0, ymaxPreferred = 10.0;
protected double xfloor = Double.NaN, xceil = Double.NaN;
protected double yfloor = Double.NaN, yceil = Double.NaN;
protected double xmin = xminPreferred, xmax = xmaxPreferred;
protected double ymin = yminPreferred, ymax = xmaxPreferred;
// pixel scale parameters These are set every time paintComponent is called using the size of the panel
protected boolean fixedPixelPerUnit = false;
protected double xPixPerUnit = 1; // the x scale in pixels per unit
protected double yPixPerUnit = 1; // the y scale in pixels per unit
protected AffineTransform pixelTransform = new AffineTransform(); // transform from world to pixel coodinates.
protected double[] pixelMatrix = new double[6]; // 6 values in the 3x3 pixel transformation
protected ArrayList<Drawable> drawableList = new ArrayList<Drawable>(); // list of Drawable objects
private volatile boolean validImage = false; // true if the current image is valid, false otherwise
protected BufferedImage offscreenImage = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB);
protected BufferedImage workingImage = offscreenImage;
private boolean buffered = false; // true will draw this component using an off-screen image
protected TextPanel trMessageBox = new TextPanel(); // text box in top right hand corner for message
protected TextPanel tlMessageBox = new TextPanel(); // text box in top left hand corner for message
protected TextPanel brMessageBox = new TextPanel(); // text box in lower right hand corner for message
protected TextPanel blMessageBox = new TextPanel(); // text box in lower left hand corner for mouse coordinates
protected DecimalFormat scientificFormat = new DecimalFormat("0.###E0"); // coordinate display format for message box. //$NON-NLS-1$
protected DecimalFormat decimalFormat = new DecimalFormat("0.00"); // coordinate display format for message box. //$NON-NLS-1$
protected MouseInputAdapter mouseController = new CMController(); // handles the coordinate display on mouse actions
protected boolean showCoordinates = false; // set to true when mouse listener is added
protected MouseInputAdapter optionController = new OptionController(); // handles optional mouse actions
protected ZoomBox zoomBox = new ZoomBox();
protected boolean enableZoom = true; // scale can be set via a mouse drag
protected boolean fixedScale = false; // scale is fixed (not user-settable)
protected Window customInspector; // optional custom inspector for this panel
protected Dimensioned dimensionSetter = null;
protected Rectangle viewRect = null; // the clipping rectangle within a scroll pane viewport
// CoordinateStringBuilder converts a mouse event into a string that displays world coordinates.
protected CoordinateStringBuilder coordinateStrBuilder = CoordinateStringBuilder.createCartesian();
protected GlassPanel glassPanel = new GlassPanel();
protected OSPLayout glassPanelLayout = new OSPLayout();
protected int refreshDelay = 100; // time in ms to delay refresh events
protected javax.swing.Timer refreshTimer = new javax.swing.Timer(refreshDelay, this); // delay before for refreshing panel
protected VideoTool vidCap;
protected double imageRatio = 1.0;
protected double xLeftMarginPercentage = 0.0, xRightMarginPercentage = 0.0;
protected double yTopMarginPercentage = 0.0, yBottomMarginPercentage = 0.0;
protected boolean logScaleX = false; // set true if the this axis uses a logarithmic scale
protected boolean logScaleY = false; // set true if the this axis uses a logarithmic scale
protected int zoomDelay = 40, zoomCount;
protected javax.swing.Timer zoomTimer;
protected double dxmin, dxmax, dymin, dymax;
protected PropertyChangeListener guiChangeListener;
/**
* DrawingPanel constructor.
*/
public DrawingPanel() {
glassPanel.setLayout(glassPanelLayout);
super.setLayout(new BorderLayout());
glassPanel.add(trMessageBox, OSPLayout.TOP_RIGHT_CORNER);
glassPanel.add(tlMessageBox, OSPLayout.TOP_LEFT_CORNER);
glassPanel.add(brMessageBox, OSPLayout.BOTTOM_RIGHT_CORNER);
glassPanel.add(blMessageBox, OSPLayout.BOTTOM_LEFT_CORNER);
glassPanel.setOpaque(false);
super.add(glassPanel, BorderLayout.CENTER);
setBackground(bgColor);
setPreferredSize(new Dimension(300, 300));
showCoordinates = true; // show coordinates by default
addMouseListener(mouseController);
addMouseMotionListener(mouseController);
addOptionController();
// invalidate the buffered image if the size changes
addComponentListener(new java.awt.event.ComponentAdapter() {
public void componentResized(ComponentEvent e) {
invalidateImage(); // validImage = false;
}
});
buildPopupmenu();
refreshTimer.setRepeats(false);
refreshTimer.setCoalesce(true);
setFontLevel(FontSizer.getLevel());
zoomTimer = new javax.swing.Timer(zoomDelay, new ActionListener() {
public void actionPerformed(ActionEvent e) {
// reset and hide the zoom box
zoomBox.xlast = zoomBox.xstop = zoomBox.xstart = 0;
zoomBox.ylast = zoomBox.ystop = zoomBox.ystart = 0;
zoomBox.visible = zoomBox.dragged = false;
int steps = 4;
if(zoomCount<steps) {
zoomCount++;
double xmin = getXMin()+dxmin/steps;
double xmax = getXMax()+dxmax/steps;
double ymin = getYMin()+dymin/steps;
double ymax = getYMax()+dymax/steps;
setPreferredMinMax(xmin, xmax, ymin, ymax);
repaint(); // repaint the panel with the new scale
} else {
zoomTimer.stop();
invalidateImage();
repaint();
}
}
});
zoomTimer.setInitialDelay(0);
// create guiChangeListener to change font size and refresh GUI
// added by D Brown 29 mar 2016
guiChangeListener = new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent e) {
if (e.getPropertyName().equals("level")) { //$NON-NLS-1$
int level = ((Integer) e.getNewValue()).intValue();
setFontLevel(level);
}
else if (e.getPropertyName().equals("locale")) { //$NON-NLS-1$
refreshGUI();
}
}
};
FontSizer.addPropertyChangeListener("level", guiChangeListener); //$NON-NLS-1$
ToolsRes.addPropertyChangeListener("locale", guiChangeListener); //$NON-NLS-1$
}
/**
* Refreshes the user interface in response to display changes such as Language.
*/
protected void refreshGUI() {
zoomInItem.setText(DisplayRes.getString("DisplayPanel.Zoom_in_menu_item")); //$NON-NLS-1$
zoomOutItem.setText(DisplayRes.getString("DisplayPanel.Zoom_out_menu_item")); //$NON-NLS-1$
scaleItem.setText(DisplayRes.getString("DrawingFrame.Scale_menu_item")); //$NON-NLS-1$
autoscaleItem.setText(DisplayRes.getString("DrawingFrame.Autoscale_menu_item")); //$NON-NLS-1$
snapshotItem.setText(DisplayRes.getString("DisplayPanel.Snapshot_menu_item")); //$NON-NLS-1$
propertiesItem.setText(DisplayRes.getString("DrawingFrame.InspectMenuItem")); //$NON-NLS-1$
}
/**
* Sets the font level.
*
* @param level the level
*/
protected void setFontLevel(int level) {
Font font = FontSizer.getResizedFont(trMessageBox.font, level);
trMessageBox.font = font;
tlMessageBox.font = font;
brMessageBox.font = font;
blMessageBox.font = font;
invalidateImage(); // validImage = false;
}
/**
* Sets the font factor.
*
* @param factor the factor
*/
public void setFontFactor(double factor) {
Font font = FontSizer.getResizedFont(trMessageBox.font, factor);
trMessageBox.font = font;
tlMessageBox.font = font;
brMessageBox.font = font;
blMessageBox.font = font;
invalidateImage(); // validImage = false;
repaint();
}
/**
* Builds the default popup menu for this panel.
*/
protected void buildPopupmenu() {
popupmenu.removeAll();
popupmenu.setEnabled(true);
ActionListener listener = new PopupmenuListener();
if(isZoom()) {
zoomInItem = new JMenuItem(DisplayRes.getString("DisplayPanel.Zoom_in_menu_item")); //$NON-NLS-1$
zoomInItem.addActionListener(listener);
popupmenu.add(zoomInItem);
zoomOutItem = new JMenuItem(DisplayRes.getString("DisplayPanel.Zoom_out_menu_item")); //$NON-NLS-1$
zoomOutItem.addActionListener(listener);
popupmenu.add(zoomOutItem);
}
if(!isFixedScale()) {
autoscaleItem = new JMenuItem(DisplayRes.getString("DrawingFrame.Autoscale_menu_item")); //$NON-NLS-1$
autoscaleItem.addActionListener(listener);
popupmenu.add(autoscaleItem);
scaleItem = new JMenuItem(DisplayRes.getString("DrawingFrame.Scale_menu_item")); //$NON-NLS-1$
scaleItem.addActionListener(listener);
popupmenu.add(scaleItem);
popupmenu.addSeparator();
}
snapshotItem = new JMenuItem(DisplayRes.getString("DisplayPanel.Snapshot_menu_item")); //$NON-NLS-1$
snapshotItem.addActionListener(listener);
popupmenu.add(snapshotItem);
popupmenu.addSeparator();
propertiesItem = new JMenuItem(DisplayRes.getString("DrawingFrame.InspectMenuItem")); //$NON-NLS-1$
propertiesItem.addActionListener(listener);
popupmenu.add(propertiesItem);
}
/**
* Sets the size of the margin during an autoscale operation.
*
* @param _autoscaleMargin
*/
public void setAutoscaleMargin(double _autoscaleMargin) {
if(autoscaleMargin==_autoscaleMargin) {
return;
}
autoscaleMargin = _autoscaleMargin;
invalidateImage(); // validImage = false;
}
/**
* Sets the extra percentage on the X left and right margins during an autoscale operation.
*
* @param _percentage
*/
public void setXMarginPercentage(double _percentage) {
if((xLeftMarginPercentage==_percentage)&&(xRightMarginPercentage==_percentage)) {
return;
}
xLeftMarginPercentage = xRightMarginPercentage = _percentage;
invalidateImage(); // validImage = false;
}
/**
* Sets the extra percentage on the X left and right margins during an autoscale operation.
*
* @param _percentage
*/
public void setXMarginPercentage(double _leftPercentage, double _rightPercentage) {
if((xLeftMarginPercentage==_leftPercentage)&&(xRightMarginPercentage==_rightPercentage)) {
return;
}
xLeftMarginPercentage = _leftPercentage;
xRightMarginPercentage = _rightPercentage;
invalidateImage(); // validImage = false;
}
/**
* Sets the extra percentage on the X left margin during an autoscale operation.
*
* @param _percentage
*/
public void setXLeftMarginPercentage(double _percentage) {
if(xLeftMarginPercentage==_percentage) {
return;
}
xLeftMarginPercentage = _percentage;
invalidateImage(); // validImage = false;
}
/**
* Sets the extra percentage on the X left margin during an autoscale operation.
*
* @param _percentage
*/
public void setXRightMarginPercentage(double _percentage) {
if(xRightMarginPercentage==_percentage) {
return;
}
xRightMarginPercentage = _percentage;
invalidateImage(); // validImage = false;
}
/**
* Sets the extra percentage on the Y top and bottom margin during an autoscale operation.
*
* @param _percentage
*/
public void setYMarginPercentage(double _percentage) {
if((yTopMarginPercentage==_percentage)&&(yBottomMarginPercentage==_percentage)) {
return;
}
yTopMarginPercentage = yBottomMarginPercentage = _percentage;
invalidateImage(); // validImage = false;
}
/**
* Sets the extra percentage on the Y top and bottom margin during an autoscale operation.
*
* @param _percentage
*/
public void setYMarginPercentage(double _bottomPercentage, double _topPercentage) {
if((yBottomMarginPercentage==_bottomPercentage)&&(yTopMarginPercentage==_topPercentage)) {
return;
}
yTopMarginPercentage = _topPercentage;
yBottomMarginPercentage = _bottomPercentage;
invalidateImage(); // validImage = false;
}
/**
* Sets the extra percentage on the Y top margin during an autoscale operation.
*
* @param _percentage
*/
public void setYTopMarginPercentage(double _percentage) {
if(yTopMarginPercentage==_percentage) {
return;
}
yTopMarginPercentage = _percentage;
invalidateImage(); // validImage = false;
}
/**
* Sets the extra percentage on the X left margin during an autoscale operation.
*
* @param _percentage
*/
public void setYBottomMarginPercentage(double _percentage) {
if(yBottomMarginPercentage==_percentage) {
return;
}
yBottomMarginPercentage = _percentage;
invalidateImage(); // validImage = false;
}
/**
* Sets the panel to exclude the gutter from the drawing.
*
* @param clip <code>true<\code> to clip; <code>false<\code> otherwise
*/
public void setClipAtGutter(boolean clip) {
if(clipAtGutter==clip) {
return;
}
clipAtGutter = clip;
invalidateImage(); // validImage = false;
}
/**
* Gets the clip at gutter flag.
*
* @return <code>true<\code> if drawing is clipped at the gutter; <code>false<\code> otherwise
*/
public boolean isClipAtGutter() {
return clipAtGutter;
}
/**
* Sets adjustable gutters. Axes are allowed to adjust the gutter size.
*
* @param fixed <code>true<\code> if gutters remain constant
*/
public void setAdjustableGutter(boolean adjustable) {
if(adjustableGutter==adjustable) {
return;
}
adjustableGutter = adjustable;
invalidateImage(); // validImage = false;
}
/**
* Gets the adjustableGutter flag. Adjustable gutters change as the panel is resized.
*
* @return <code>true<\code> if gutters are adjustable
*/
public boolean isAdjustableGutter() {
return adjustableGutter;
}
/**
* Sets the mouse cursor.
* @param cursor
*/
public void setMouseCursor(Cursor cursor) {
Container c = getTopLevelAncestor();
setCursor(cursor);
if(c!=null) {
c.setCursor(cursor);
}
}
/**
* Checks the image to see if the working image has the correct Dimension.
*
* Checking is done in the event dispatch thread.
*
* @return <code>true <\code> if the offscreen image matches the panel; <code>false <\code> otherwise
*/
protected boolean checkWorkingImage() {
Runnable runImageCheck = new Runnable() {
public void run() {
workingImage();
}
};
if(SwingUtilities.isEventDispatchThread()) {
return workingImage();
}
try {
SwingUtilities.invokeAndWait(runImageCheck);
return true;
} catch(Exception ex) {
OSPLog.finest("Exception in Check Working Image:"+ex.toString()); //$NON-NLS-1$
return false;
}
}
/**
* Checks the image to see if the working image has the correct Dimension.
*
* @return <code>true <\code> if the offscreen image matches the panel; <code>false <\code> otherwise
*/
private boolean workingImage() {
Rectangle r = getBounds();
int width = (int) r.getWidth();
int height = (int) r.getHeight();
if((width<=2)||(height<=2)) {
return false; // panel is too small to draw anything useful
}
if((workingImage==null)||(width!=workingImage.getWidth())||(height!=workingImage.getHeight())) {
this.workingImage = getGraphicsConfiguration().createCompatibleImage(width, height);
invalidateImage(); // validImage = false; // buffer image is not valid
}
if(this.workingImage==null) { // image could not be created
invalidateImage(); // validImage = false; // buffer image is not valid
return false;
}
return true; // the buffered image has been created and is the correct size
}
/**
* Performs the action for the refresh timer by rendering (redrawing) the panel.
*
* @param evt
*/
public void actionPerformed(ActionEvent evt) {
if(!isValidImage()) {
render();
}
}
/**
* Gets the iconified flag from the top level frame.
*
* @return boolean true if frame is iconified; false otherwise
*/
public boolean isIconified() {
Component c = getTopLevelAncestor();
if(c instanceof Frame) {
return(((Frame) c).getExtendedState()&Frame.ICONIFIED)==1;
}
return false;
}
/**
* Paints all drawables onto an offscreen image buffer and copies this image onto the screen.
*
* @return the image buffer
*/
public BufferedImage render() {
if(!isShowing()||isIconified()) {
return offscreenImage; // no need to draw if the frame is not visible
}
if(buffered&&checkWorkingImage()) {
validImage = true; // drawing into the working image will produce a valid image
render(workingImage);
// swap the images
BufferedImage temp = offscreenImage;
offscreenImage = workingImage;
workingImage = temp;
}
// always update a Swing component from the event thread
Runnable doNow = new Runnable() {
public void run() {
paintImmediately(getVisibleRect());
}
};
try {
if(SwingUtilities.isEventDispatchThread()) {
paintImmediately(getVisibleRect());
} else { // paint within the event thread
SwingUtilities.invokeAndWait(doNow);
}
} catch(InvocationTargetException ex1) {}
catch(InterruptedException ex1) {}
if(vidCap!=null) {
if(buffered) { // buffered image exists so use it.
vidCap.addFrame(offscreenImage);
} else { // render the image if the buffer does not exist
// inefficient as the image may be rendered twice during every animation step
if(vidCap.isRecording()) {
vidCap.addFrame(render());
}
}
}
return offscreenImage;
}
/**
* Paints all drawables onto an image.
*
* @param image
* @return the image buffer
*/
public BufferedImage render(BufferedImage image) {
Graphics osg = image.getGraphics();
imageRatio =((float) getWidth()<=0)?1: image.getWidth()/(float) getWidth(); // ratio of image to panel width
if(osg!=null) {
paintEverything(osg);
if(image==workingImage) {
zoomBox.paint(osg); // paint the zoom
}
Rectangle viewRect = this.viewRect; // reference for thread safety
if(viewRect!=null) {
Rectangle r = new Rectangle(0, 0, image.getWidth(null), image.getHeight(null));
glassPanel.setBounds(r);
glassPanelLayout.checkLayoutRect(glassPanel, r);
glassPanel.render(osg);
glassPanel.setBounds(viewRect);
glassPanelLayout.checkLayoutRect(glassPanel, viewRect);
} else {
glassPanel.render(osg);
}
osg.dispose();
}
imageRatio = 1.00;
return image;
}
public int getWidth() {
return(int) (imageRatio*super.getWidth()); // effective width when rendering images
}
public int getHeight() {
return(int) (imageRatio*super.getHeight()); // effective height when rendering images
}
/**
* Gets the ratio of the drawing image to the panel.
* @return double
*/
public double getImageRatio() {
return imageRatio;
}
/**
* Invalidate the offscreen image so that it is rendered during the next repaint operation if buffering is enabled.
*/
public void invalidateImage() {
validImage = false;
}
/**
* Validate the offscreen image to insure that the render method will execute.
*/
public void validateImage() {
validImage = true;
}
protected boolean isValidImage() {
return validImage;
}
public void paint(Graphics g) {
Graphics2D g2 = (Graphics2D) g;
boolean resetBuffered = buffered;
if(g2.getDeviceConfiguration().getDevice().getType()==GraphicsDevice.TYPE_PRINTER) {
buffered = false;
//System.out.println("buffer off");
}
super.paint(g);
buffered = resetBuffered;
}
/**
* Paints this component.
* @param g
*/
public void paintComponent(Graphics g) {
if(OSPRuntime.disableAllDrawing) {
g.setColor(bgColor);
g.fillRect(0, 0, getWidth(), getHeight());
return;
}
viewRect = findViewRect(); // find the clipping rectangle within a scroll pane viewport
if(buffered) { // paint bufferImage onto screen
if(!validImage||(getWidth()!=offscreenImage.getWidth())||(getHeight()!=offscreenImage.getHeight())) {
if((getWidth()!=offscreenImage.getWidth())||(getHeight()!=offscreenImage.getHeight())) {
g.setColor(Color.WHITE);
g.fillRect(0, 0, getWidth(), getHeight());
} else {
g.drawImage(offscreenImage, 0, 0, null); // copy old image to the screen for now
}
refreshTimer.start(); // image is not valid so start refresh timer
} else { // current image is valid and has correct size
g.drawImage(offscreenImage, 0, 0, null); // copy image to the screen
}
} else { // paint directly onto the graphics buffer
validImage = true; // painting everything gives a valid onscreen image
paintEverything(g);
}
zoomBox.paint(g);
// if(enableZoom||zoomMode) { // zoom box is always painted on top
// zoomBox.paint(g);
// }
}
/**
* Gets the clipping rectangle within a scroll pane viewport.
*
* @return the clipping rectangle
*/
protected Rectangle getViewRect() {
return viewRect;
}
/**
* Finds the clipping rectangle if this panel is within a scroll pane viewport.
*/
protected Rectangle findViewRect() {
Rectangle rect = null;
Container c = getParent();
while(c!=null) {
if(c instanceof JViewport) {
rect = ((JViewport) c).getViewRect();
glassPanel.setBounds(rect);
glassPanelLayout.checkLayoutRect(glassPanel, rect);
break;
}
c = c.getParent();
}
return rect;
}
/**
* Computes the size of the gutters. Objects, such as axes, can perform this method
* by implementing the Dimensioned interface.
*/
protected void computeGutters() {
if(dimensionSetter!=null) {
Dimension interiorDimension = dimensionSetter.getInterior(this);
if(interiorDimension!=null) {
squareAspect = false;
leftGutter = rightGutter = Math.max(0, getWidth()-interiorDimension.width)/2;
topGutter = bottomGutter = Math.max(0, getHeight()-interiorDimension.height)/2;
}
}
}
/**
* Paints before the panel iterates through its list of Drawables.
* @param g Graphics
*/
protected void paintFirst(Graphics g) {
g.setColor(getBackground());
g.fillRect(0, 0, getWidth(), getHeight()); // fill the component with the background color
g.setColor(Color.black); // restore the default drawing color
}
/**
* Paints after the panel iterates through its list of Drawables.
* @param g Graphics
*/
protected void paintLast(Graphics g) {}
/**
* Paints everything inside this component.
*
* @param g
*/
protected void paintEverything(Graphics g) {
if(RECORD_PAINT_TIMES) {
long time = System.currentTimeMillis();
System.out.println("elapsed time(s)="+((int) (time-currentTime)/1000.0)); //$NON-NLS-1$
currentTime = time;
}
// the following statement has been moved to paintComponent
// viewRect = findViewRect(); // finds the clipping rectangle within a scroll pane viewport
computeGutters(); // last chance to set the gutters
ArrayList<Drawable> tempList = getDrawables(); // holds a clone of the drawable object list
scale(tempList); // sets the world-coordinate scale based on the autoscale values
setPixelScale(); // sets the pixel scale and the world-to-pixel affine transformation matrix
if (!OSPRuntime.isMac()) { //Rendering hint bug in Mac Snow Leopard
if (antialiasTextOn) {
((Graphics2D) g).setRenderingHint(
RenderingHints.KEY_TEXT_ANTIALIASING,
RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
} else {
((Graphics2D) g).setRenderingHint(
RenderingHints.KEY_TEXT_ANTIALIASING,
RenderingHints.VALUE_TEXT_ANTIALIAS_OFF);
}
if (antialiasShapeOn) {
((Graphics2D) g).setRenderingHint(
RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
} else {
((Graphics2D) g).setRenderingHint(
RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_OFF);
}
}
// ready to draw everything
if(!validImage) {
return; // abort drawing
}
paintFirst(g); // PlottingPanel uses this method to paint axes
if(!validImage) {
return; // abort drawing
}
paintDrawableList(g, tempList);
if(!validImage) {
return; // abort drawing
}
paintLast(g); // does nothing yet but can be used to add a legend, etc
if(RECORD_PAINT_TIMES) {
System.out.println("paint time (ms)="+(int) (System.currentTimeMillis()-currentTime)+'\n'); //$NON-NLS-1$
}
}
/**
* Autoscale the x axis using min and max values.
* from measurable objects.
* @param autoscale
*/
public void setAutoscaleX(boolean autoscale) {
if((autoscaleX==autoscale)&&(autoscaleXMax==autoscale)&&(autoscaleXMin==autoscale)) {
return;
}
autoscaleX = autoscaleXMax = autoscaleXMin = autoscale;
invalidateImage(); // validImage = false;
}
/**
* Determines if the x axis autoscale property is true.
* @return <code>true<\code> if autoscaled.
*/
public boolean isAutoscaleX() {
return autoscaleX;
}
/**
* Determines if the horizontal maximum value is autoscaled.
* @return <code>true<\code> if xmax is autoscaled.
*/
public boolean isAutoscaleXMax() {
return autoscaleXMax;
}
/**
* Determines if the horizontal minimum value is autoscaled.
* @return <code>true<\code> if xmin is autoscaled.
*/
public boolean isAutoscaleXMin() {
return autoscaleXMin;
}
/**
* Autoscale the y axis using min and max values.
* from measurable objects.
* @param autoscale
*/
public void setAutoscaleY(boolean autoscale) {
if((autoscaleY==autoscale)&&(autoscaleYMax==autoscale)&&(autoscaleYMin==autoscale)) {
return;
}
autoscaleY = autoscaleYMax = autoscaleYMin = autoscale;
invalidateImage(); // validImage = false;
}
/**
* Determines if the y axis autoscale property is true.
* @return <code>true<\code> if autoscaled.
*/
public boolean isAutoscaleY() {
return autoscaleY;
}
/**
* Determines if the vertical maximum value is autoscaled.
* @return <code>true<\code> if ymax is autoscaled.
*/
public boolean isAutoscaleYMax() {
return autoscaleYMax;
}
/**
* Determines if the vertical minimum value is autoscaled.
* @return <code>true<\code> if ymin is autoscaled.
*/
public boolean isAutoscaleYMin() {
return autoscaleYMin;
}
/**
* Gets the logScaleX value.
*
* @return boolean
*/
public boolean isLogScaleX() {
return logScaleX;
}
/**
* Gets the logScaleY value.
*
* @return boolean
*/
public boolean isLogScaleY() {
return logScaleY;
}
/**
* Moves and resizes this component. The new location of the top-left
* corner is specified by <code>x</code> and <code>y</code>, and the
* new size is specified by <code>width</code> and <code>height</code>.
* @param x The new <i>x</i>-coordinate of this component.
* @param y The new <i>y</i>-coordinate of this component.
* @param width The new <code>width</code> of this component.
* @param height The new <code>height</code> of this
* component.
*/
public void setBounds(int x, int y, int width, int height) {
if((getBounds().x==x)&&(getBounds().y==y)&&(getBounds().width==width)&&(getBounds().height==height)) {
return;
}
super.setBounds(x, y, width, height);
invalidateImage(); // validImage = false;
}
public void setBounds(Rectangle r) {
if(getBounds().equals(r)) {
return;
}
super.setBounds(r);
invalidateImage(); // validImage = false;
}
/**
* Sets the buffered image option.
*
* Buffered panels copy the offscreen image into the panel during a repaint unless the image
* has been invalidated. Use the render() method to draw the image immediately.
*
* @param _buffered
*/
public void setBuffered(boolean _buffered) {
if(buffered==_buffered) {
return;
}
buffered = _buffered;
if(buffered) { // turn off Java buffering because we are doing our own
setDoubleBuffered(false);
} else { // small default image is not used
workingImage = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB);
offscreenImage = workingImage;
setDoubleBuffered(true); // use Java's buffer
}
invalidateImage(); // validImage = false;
}
public boolean isBuffered() {
return buffered;
}
/**
* Makes the component visible or invisible.
* Overrides <code>JComponent.setVisible</code>.
*
* @param vis true to make the component visible; false to
* make it invisible
*/
public void setVisible(boolean vis) {
if(this.isVisible()==vis) {
return;
}
super.setVisible(vis);
invalidateImage(); // validImage = false;
}
/**
* Limits the xmin and xmax values during autoscaling so that the mininimum value
* will be no greater than the floor and the maximum value will be no
* smaller than the ceil.
*
* Setting a floor or ceil value to <code>Double.NaN<\code> will disable that limit.
*
* @param floor the xfloor value
* @param ceil the xceil value
*/
public void limitAutoscaleX(double floor, double ceil) {
if(ceil-floor<Float.MIN_VALUE) { // insures that floor and ceiling some separation
floor = 0.9*floor-Float.MIN_VALUE;
ceil = 1.1*ceil+Float.MIN_VALUE;
}
xfloor = floor;
xceil = ceil;
}
/**
* Limits ymin and ymax values during autoscaling so that the mininimum value
* will be no greater than the floor and the maximum value will be no
* smaller than the ceil.
*
* Setting a floor or ceil value to <code>Double.NaN<\code> will disable that limit.
*
* @param floor the yfloor value
* @param ceil the yceil value
*/
public void limitAutoscaleY(double floor, double ceil) {
if(ceil-floor<Float.MIN_VALUE) { // insures that floor and ceiling some separation
floor = 0.9*floor-Float.MIN_VALUE;
ceil = 1.1*ceil+Float.MIN_VALUE;
}
yfloor = floor;
yceil = ceil;
}
/**
* Sets the scale using pixels per unit.
*
* @param enable boolean enable fixed pixels per unit
* @param xPixPerUnit double
* @param yPixPerUnit double
*/
public void setPixelsPerUnit(boolean enable, double xPixPerUnit, double yPixPerUnit) {
if((fixedPixelPerUnit==enable)&&(this.xPixPerUnit==xPixPerUnit)&&(this.yPixPerUnit==yPixPerUnit)) {
return;
}
fixedPixelPerUnit = enable;
this.xPixPerUnit = xPixPerUnit;
this.yPixPerUnit = yPixPerUnit;
invalidateImage(); // validImage = false;
}
/**
* Sets the preferred scale in the vertical and horizontal direction.
* @param xmin
* @param xmax
* @param ymin
* @param ymax
* @param invalidateImage invalidates image if min/max have changed
*/
public void setPreferredMinMax(double xmin, double xmax, double ymin, double ymax, boolean invalidateImage) {
autoscaleX = autoscaleXMin = autoscaleXMax = false;
autoscaleY = autoscaleYMin = autoscaleYMax = false;
if((xminPreferred==xmin)&&(xmaxPreferred==xmax)&&(yminPreferred==ymin)&&(ymaxPreferred==ymax)) {
return;
}
if(Double.isNaN(xmin)) {
autoscaleXMin = true;
xmin = xminPreferred;
}
if(Double.isNaN(xmax)) {
autoscaleXMax = true;
xmax = xmaxPreferred;
}
autoscaleX = autoscaleXMin||autoscaleXMax;
if(xmin==xmax) {
xmin = 0.9*xmin-0.5;
xmax = 1.1*xmax+0.5;
}
xminPreferred = xmin;
xmaxPreferred = xmax;
if(Double.isNaN(ymin)) {
autoscaleYMin = true;
ymin = yminPreferred;
}
if(Double.isNaN(ymax)) {
autoscaleYMax = true;
ymax = ymaxPreferred;
}
autoscaleY = autoscaleYMin||autoscaleYMax;
if(ymin==ymax) {
ymin = 0.9*ymin-0.5;
ymax = 1.1*ymax+0.5;
}
yminPreferred = ymin;
ymaxPreferred = ymax;
if(invalidateImage) {
invalidateImage();
}
}
/**
* Sets the preferred scale in the vertical and horizontal direction.
* @param xmin
* @param xmax
* @param ymin
* @param ymax
*/
public void setPreferredMinMax(double xmin, double xmax, double ymin, double ymax) {
setPreferredMinMax(xmin, xmax, ymin, ymax, false);
}
/**
* Sets the preferred scale in the horizontal direction.
* @param xmin the minimum value
* @param xmax the maximum value
*/
public void setPreferredMinMaxX(double xmin, double xmax) {
autoscaleX = autoscaleXMin = autoscaleXMax = false;
if((xminPreferred==xmin)&&(xmaxPreferred==xmax)) {
return;
}
if(Double.isNaN(xmin)) {
autoscaleXMin = true;
xmin = xminPreferred;
}
if(Double.isNaN(xmax)) {
autoscaleXMax = true;
xmax = xmaxPreferred;
}
autoscaleX = autoscaleXMin||autoscaleXMax;
if(xmin==xmax) {
xmin = 0.9*xmin-0.5;
xmax = 1.1*xmax+0.5;
}
xminPreferred = xmin;
xmaxPreferred = xmax;
invalidateImage();
}
/**
* Sets the preferred scale in the vertical direction.
* @param ymin
* @param ymax
*/
public void setPreferredMinMaxY(double ymin, double ymax) {
autoscaleY = autoscaleYMin = autoscaleYMax = false;
if((yminPreferred==ymin)&&(ymaxPreferred==ymax)) {
return;
}
if(Double.isNaN(ymin)) {
autoscaleYMin = true;
ymin = yminPreferred;
}
if(Double.isNaN(ymax)) {
autoscaleYMax = true;
ymax = ymaxPreferred;
}
autoscaleY = autoscaleYMin||autoscaleYMax;
if(ymin==ymax) {
ymin = 0.9*ymin-0.5;
ymax = 1.1*ymax+0.5;
}
yminPreferred = ymin;
ymaxPreferred = ymax;
invalidateImage();
}
/**
* Sets the aspect ratio for horizontal to vertical to unity when <code>true<\code>.
* @param val
*/
public void setSquareAspect(boolean val) {
if(squareAspect==val) {
return;
}
squareAspect = val;
invalidateImage(); // validImage = false;
repaint();
}
/**
* Determines if the number of pixels per unit is the same for both x and y.
* @return <code>true<\code> if squareAspect
*/
public boolean isSquareAspect() {
return squareAspect;
}
/**
* Set flag for text antialiasing.
*/
public void setAntialiasTextOn(boolean on) {
antialiasTextOn = on;
}
/**
* Gets flag for text antialiasing.
*/
public boolean isAntialiasTextOn() {
return antialiasTextOn;
}
/**
* Set flag for shape antialiasing.
*/
public void setAntialiasShapeOn(boolean on) {
antialiasShapeOn = on;
}
/**
* Gets flag for shape antialiasing.
*/
public boolean isAntialiasShapeOn() {
return antialiasShapeOn;
}
/**
* Determines if the x and y point is inside.
*
* @param x the coordinate in world units
* @param y the coordinate in world units
*
* @return <code>true<\code> if point is inside; <code>false<\code> otherwise
*/
public boolean isPointInside(double x, double y) {
if(xmin<xmax) {
if(x<xmin) {
return false;
}
if(x>xmax) {
return false;
}
} else { // max is less than min so scale decreases to the right
if(x>xmin) {
return false;
}
if(x<xmax) {
return false;
}
}
if(ymin<ymax) {
if(y<ymin) {
return false;
}
if(y>ymax) {
return false;
}
} else { // max is less than min so scale decreases to the right
if(y>ymin) {
return false;
}
if(y<ymax) {
return false;
}
}
return true;
}
/**
* Determines if the scale is fixed.
* @return <code>true<\code> if scale is fixed
*/
public boolean isFixedScale() {
return fixedScale;
}
/**
* Sets the fixed scale property. If fixed, the user cannot change the scale.
* @param fixed <code>true<\code> to prevent user changes to scale
*/
public void setFixedScale(boolean fixed) {
if(fixedScale==fixed) {
return;
}
fixedScale = fixed;
buildPopupmenu();
}
/**
* Determines if the user can change scale by dragging the mouse.
* @return <code>true<\code> if zoom is enabled and scale is not fixed
*/
public boolean isZoom() {
return enableZoom&&!isFixedScale();
}
/**
* Sets the zoom option to allow the user to change scale by dragging the mouse.
* @param _enableZoom <code>true<\code> if zoom is enabled
*/
public void setZoom(boolean _enableZoom) {
if(enableZoom==_enableZoom) {
return;
}
enableZoom = _enableZoom;
buildPopupmenu();
}
/**
* Zooms out by a factor of two.
*/
protected void zoomOut() {
// find center of zoom box
int xPix = (zoomBox.xstart+zoomBox.xstop)/2;
int yPix = (zoomBox.ystart+zoomBox.ystop)/2;
double xCenter = pixToX(xPix);
double yCenter = pixToY(yPix);
// determine distances from center to edges
double dx = Math.abs(xmax-xmin);
double dy = Math.abs(ymax-ymin);
// set up zoomTimer parameters and start
dxmin = xCenter-dx-getXMin();
dxmax = xCenter+dx-getXMax();
dymin = yCenter-dy-getYMin();
dymax = yCenter+dy-getYMax();
zoomCount = 0;
zoomTimer.start();
}
/**
* Returns the internal ZoomBox object
* @return ZoomBox
*/
public ZoomBox getZoomBox() {
return zoomBox;
}
/**
* Zooms in to the current zoom box.
*/
protected void zoomIn() {
// set up zoomTimer parameters and start
dxmin = pixToX(Math.min(zoomBox.xstart, zoomBox.xstop))-getXMin();
dxmax = pixToX(Math.max(zoomBox.xstart, zoomBox.xstop))-getXMax();
dymin = pixToY(Math.max(zoomBox.ystart, zoomBox.ystop))-getYMin();
dymax = pixToY(Math.min(zoomBox.ystart, zoomBox.ystop))-getYMax();
zoomCount = 0;
zoomTimer.start();
}
/**
* Creates a snapshot using an image of the content.
*/
public void snapshot() {
int w = (isVisible()) ? getWidth() : getPreferredSize().width;
int h = (isVisible()) ? getHeight() : getPreferredSize().height;
if((w==0)||(h==0)) {
return;
}
BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
render(image);
MeasuredImage mi = new MeasuredImage(image, pixToX(0), pixToX(w), pixToY(h), pixToY(0));
// create ImageFrame using reflection--code change by D Brown 1/6/14
OSPFrame frame = null;
try {
Class<?> c = Class.forName("org.opensourcephysics.frames.ImageFrame"); //$NON-NLS-1$
Constructor<?>[] constructors = c.getConstructors();
for(int i = 0; i<constructors.length; i++) {
Class<?>[] parameters = constructors[i].getParameterTypes();
if(parameters.length==1 && parameters[0]==MeasuredImage.class) {
frame = (OSPFrame) constructors[i].newInstance(new Object[] {mi});
break;
}
}
} catch (Exception ex) {
ex.printStackTrace();
}
if (frame==null) return;
frame.setTitle(DisplayRes.getString("Snapshot.Title")); //$NON-NLS-1$
frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
frame.setKeepHidden(false);
FontSizer.setFonts(frame, FontSizer.getLevel());
frame.pack();
frame.setVisible(true);
}
/**
* Determines if the user can examine and change the scale at run-time by right-clicking.
* @return <code>true<\code> if inspector is enabled
*/
public boolean hasInspector() {
return(popupmenu!=null)&&popupmenu.isEnabled();
}
/**
* Enables the popup inspector option.
* The default inspector shows a popup menu by right-clicking.
*
* @param isEnabled <code>true<\code> if the inspector option is enabled; <code>false<\code> otherwise
*/
public void enableInspector(boolean isEnabled) {
popupmenu.setEnabled(isEnabled);
}
/**
* Gets the popup menu.
*/
public JPopupMenu getPopupMenu() {
return popupmenu;
}
/**
* Sets the popup menu.
*/
public void setPopupMenu(JPopupMenu menu) {
popupmenu = menu;
}
/**
* Shows the drawing panel properties inspector.
*/
public void showInspector() {
if(customInspector==null) {
// DrawingPanelInspector.getInspector(this); // this static inspector is the same for all drawing panels.
XMLDrawingPanelInspector.getInspector(this); // this static inspector is the same for all drawing panels.
} else {
customInspector.setVisible(true);
}
}
/**
* Hides the drawing panel properties inspector.
*/
public void hideInspector() {
if(customInspector==null) {
DrawingPanelInspector.hideInspector(); // this static inspector is the same for all drawing panels.
} else {
customInspector.setVisible(false);
}
}
/**
* Sets a custom properties inspector window.
*
* @param w the new inspector window
*/
public void setCustomInspector(Window w) {
if(customInspector!=null) {
customInspector.setVisible(false); // hide the current inspector window
}
customInspector = w;
}
/**
* Sets the video tool. May be set to null.
*
* @param videoCap the video capture tool
*/
public void setVideoTool(VideoTool videoCap) {
if(vidCap!=null) {
vidCap.setVisible(false); // hide the current video capture tool
}
vidCap = videoCap;
if(vidCap!=null) {
setBuffered(true);
}
}
/**
* Gets the video capture tool. May be null.
*
* @return the video capture tool
*/
public VideoTool getVideoTool() {
return vidCap;
}
/**
* Gets the ratio of pixels per unit in the x and y directions.
* @return the aspect ratio
*/
public double getAspectRatio() {
return(pixelMatrix[3]==1) ? 1 : Math.abs(pixelMatrix[0]/pixelMatrix[3]);
}
/**
* Gets the number of pixels per world unit in the x direction.
* @return pixels per unit
*/
public double getXPixPerUnit() {
return pixelMatrix[0];
}
/**
* Gets the number of pixels per world unit in the y direction.
* Y pixels per unit is positive if y increases from bottom to top.
*
* @return pixels per unit
*/
public double getYPixPerUnit() {
return -pixelMatrix[3];
}
/**
* Gets the larger of x or y pixels per world unit.
* @return pixels per unit
*/
public double getMaxPixPerUnit() {
return Math.max(Math.abs(pixelMatrix[0]), Math.abs(pixelMatrix[3]));
}
/**
* Gets the x world coordinate for the left-hand side of the drawing area.
* @return xmin
*/
public double getXMin() {
return xmin;
}
/**
* Gets the preferred x world coordinate for the left-hand side of the drawing area.
* @return xmin
*/
public double getPreferredXMin() {
return xminPreferred;
}
/**
* Gets the x world coordinate for the right-hand side of the drawing area.
* @return xmax
*/
public double getXMax() {
return xmax;
}
/**
* Gets the preferred x world coordinate for the right-hand side of the drawing area.
* @return xmin
*/
public double getPreferredXMax() {
return xmaxPreferred;
}
/**
* Gets the y world coordinate for the top of the drawing area.
* @return ymax
*/
public double getYMax() {
return ymax;
}
/**
* Gets the preferred y world coordinate for the top of the drawing area.
* @return xmin
*/
public double getPreferredYMax() {
return ymaxPreferred;
}
/**
* Gets the y world coordinate for the bottom of the drawing area.
* @return ymin
*/
public double getYMin() {
return ymin;
}
/**
* Gets the preferred y world coordinate for the bottom of the drawing area.
* @return xmin
*/
public double getPreferredYMin() {
return yminPreferred;
}
/**
* Gets the CoordinateStringBuilder that converts mouse events into a string showing world coordinates.
* @return CoordinateStringBuilder
*/
public CoordinateStringBuilder getCoordinateStringBuilder() {
return coordinateStrBuilder;
}
/**
* Sets the CoordinateStringBuilder that converts mouse events into a string showing world coordinates.
*/
public void setCoordinateStringBuilder(CoordinateStringBuilder builder) {
coordinateStrBuilder = builder;
}
/**
* Gets the scale that will be used when the panel is drawn.
* @return Rectangle2D
*/
public Rectangle2D getScale() {
setPixelScale();
return new Rectangle2D.Double(xmin, ymin, xmax-xmin, ymax-ymin);
}
/**
* Gets the rectangle that bounds all measurable objects.
*
* @return Rectangle2D
*/
public Rectangle2D getMeasure() {
double xmin = Double.MAX_VALUE;
double xmax = -Double.MAX_VALUE;
double ymin = Double.MAX_VALUE;
double ymax = -Double.MAX_VALUE;
boolean measurableFound = false;
ArrayList<Drawable> tempList = getDrawables(); // this clones the list of drawables
Iterator<Drawable> it = tempList.iterator();
while(it.hasNext()) {
Object obj = it.next();
if(!(obj instanceof Measurable)||!((Measurable) obj).isMeasured()) {
continue; // object is not measurable or measure is not set
}
Measurable measurable = (Measurable) obj;
double gxmax = measurable.getXMax();
double gxmin = measurable.getXMin();
if(logScaleX&&(measurable instanceof LogMeasurable)) {
gxmax = ((LogMeasurable) measurable).getXMaxLogscale();
gxmin = ((LogMeasurable) measurable).getXMinLogscale();
}
double gymax = measurable.getYMax();
double gymin = measurable.getYMin();
//System.out.println("measurable="+measurable+" is Log Measurable?"+ (measurable instanceof LogMeasurable));
if(logScaleY&&(measurable instanceof LogMeasurable)) {
gymax = ((LogMeasurable) measurable).getYMaxLogscale();
gymin = ((LogMeasurable) measurable).getYMinLogscale();
//System.out.println("ymin="+gymin+ " ymax="+gymax+ " measurable="+measurable);
}
if(!Double.isNaN(gxmax)&&!Double.isNaN(gxmin)&&!Double.isNaN(gymax)&&!Double.isNaN(gymin)) {
xmin = Math.min(xmin, gxmin);
xmax = Math.max(xmax, gxmax);
ymin = Math.min(ymin, gymin);
ymax = Math.max(ymax, gymax);
measurableFound = true; // we have at least one valid min-max measure
}
}
if(measurableFound) {
return new Rectangle2D.Double(xmin, ymin, xmax-xmin, ymax-ymin);
}
return new Rectangle2D.Double(0, 0, 0, 0);
}
/**
* Gets the affine transformation that converts from world to pixel coordinates.
* @return the affine transformation
*/
public AffineTransform getPixelTransform() {
return(AffineTransform) pixelTransform.clone();
}
/**
* Retrieves the 6 specifiable values in the pixel transformation
* matrix and places them into an array of double precisions values.
* The values are stored in the array as
* { m00 m10 m01 m11 m02 m12 }.
*
* @return the transformation matrix
*/
public double[] getPixelMatrix() {
return pixelMatrix;
}
/**
* Calculates min and max values and the affine transformation based on the
* current size of the panel and the squareAspect boolean.
*/
public void setPixelScale() {
xmin = xminPreferred; // start with the preferred min-max values.
xmax = xmaxPreferred;
ymin = yminPreferred;
ymax = ymaxPreferred;
leftGutter = leftGutterPreferred; // start with default gutter values
topGutter = topGutterPreferred;
rightGutter = rightGutterPreferred;
bottomGutter = bottomGutterPreferred;
width = getWidth();
height = getHeight();
if(fixedPixelPerUnit) { // the user has specified a fixed pixel scale
xmin = (xmaxPreferred+xminPreferred)/2-Math.max(width-leftGutter-rightGutter-1, 1)/xPixPerUnit/2;
xmax = (xmaxPreferred+xminPreferred)/2+Math.max(width-leftGutter-rightGutter-1, 1)/xPixPerUnit/2;
ymin = (ymaxPreferred+yminPreferred)/2-Math.max(height-bottomGutter-topGutter-1, 1)/yPixPerUnit/2;
ymax = (ymaxPreferred+yminPreferred)/2+Math.max(height-bottomGutter-topGutter-1, 1)/yPixPerUnit/2;
pixelTransform = new AffineTransform(xPixPerUnit, 0, 0, -yPixPerUnit, -xmin*xPixPerUnit+leftGutter, ymax*yPixPerUnit+topGutter);
pixelTransform.getMatrix(pixelMatrix); // puts the transformation into the pixel matrix
return;
}
xPixPerUnit = Math.max(width-leftGutter-rightGutter, 1)/(xmax-xmin);
yPixPerUnit = Math.max(height-bottomGutter-topGutter, 1)/(ymax-ymin); // the y scale in pixels
if(squareAspect) {
double stretch = Math.abs(xPixPerUnit/yPixPerUnit);
if(stretch>=1) { // make the x range bigger so that aspect ratio is one
stretch = Math.min(stretch, width); // limit the stretch
xmin = xminPreferred-(xmaxPreferred-xminPreferred)*(stretch-1)/2.0;
xmax = xmaxPreferred+(xmaxPreferred-xminPreferred)*(stretch-1)/2.0;
xPixPerUnit = Math.max(width-leftGutter-rightGutter, 1)/(xmax-xmin); // the x scale in pixels per unit
} else { // make the y range bigger so that aspect ratio is one
stretch = Math.max(stretch, 1.0/height); // limit the stretch
ymin = yminPreferred-(ymaxPreferred-yminPreferred)*(1.0/stretch-1)/2.0;
ymax = ymaxPreferred+(ymaxPreferred-yminPreferred)*(1.0/stretch-1)/2.0;
yPixPerUnit = Math.max(height-bottomGutter-topGutter, 1)/(ymax-ymin); // the y scale in pixels per unit
}
}
pixelTransform = new AffineTransform(xPixPerUnit, 0, 0, -yPixPerUnit, -xmin*xPixPerUnit+leftGutter, ymax*yPixPerUnit+topGutter);
pixelTransform.getMatrix(pixelMatrix); // puts the transformation into the pixel matrix
}
/**
* Recomputes the pixel transforamtion based on the current minimum and maximum values and the gutters.
*/
public void recomputeTransform() {
xPixPerUnit = Math.max(width-leftGutter-rightGutter, 1)/(xmax-xmin);
yPixPerUnit = Math.max(height-bottomGutter-topGutter, 1)/(ymax-ymin); // the y scale in pixels
pixelTransform = new AffineTransform(xPixPerUnit, 0, 0, -yPixPerUnit, -xmin*xPixPerUnit+leftGutter, ymax*yPixPerUnit+topGutter);
pixelTransform.getMatrix(pixelMatrix); // puts the transformation into the pixel matrix
}
/**
* Projects a 2D or 3D world coordinate to a pixel coordinate.
*
* An (x, y) point will project to (xpix, ypix).
* An (x, y, z) point will project to (xpix, ypix).
* An (x, y, delta_x, delta_y) point will project to (xpix, ypix, delta_xpix, delta_ypix).
* An (x, y, z, delta_x, delta_y, delta_z) point will project to (xpix, ypix, delta_xpix, delta_ypix).
*
* @param coordinate
* @param pixel
* @return pixel
*/
public double[] project(double[] coordinate, double[] pixel) {
switch(coordinate.length) {
case 2 : // input is x,y
case 3 : // input is x,y,z
pixel[0] = xToGraphics(coordinate[0]); // x
pixel[1] = yToGraphics(coordinate[1]); // y
break;
case 4 : // input is x,y,dx,dy
pixel[0] = xToGraphics(coordinate[0]); // x
pixel[1] = yToGraphics(coordinate[1]); // y
pixel[2] = xPixPerUnit*coordinate[2]; // dx
pixel[3] = yPixPerUnit*coordinate[3]; // dy
break;
case 6 : // input is x,y,z,dx,dy,dz
pixel[0] = xToGraphics(coordinate[0]); // x
pixel[1] = yToGraphics(coordinate[1]); // y
pixel[2] = xPixPerUnit*coordinate[3]; // dx
pixel[3] = yPixPerUnit*coordinate[4]; // dy
break;
default :
throw new IllegalArgumentException("Method project not supported for this length."); //$NON-NLS-1$
}
return pixel;
}
/**
* Converts pixel to x world units.
* @param pix
* @return x panel units
*/
public double pixToX(int pix) {
return xmin+(pix-leftGutter)/xPixPerUnit;
}
/**
* Converts x from world to pixel units.
* @param x
* @return the pixel value of the x coordinate
*/
public int xToPix(double x) {
double pix = pixelMatrix[0]*x+pixelMatrix[4];
if(pix>Integer.MAX_VALUE) {
return Integer.MAX_VALUE;
}
if(pix<Integer.MIN_VALUE) {
return Integer.MIN_VALUE;
}
// return (int)Math.round(pix);
return(int) Math.floor((float) pix); // gives better registration with affine transformation
}
/**
* Converts x from world to graphics device units.
* @param x
* @return the graphics device value of the x coordinate
*/
public float xToGraphics(double x) {
float pix = (float) (pixelMatrix[0]*x+pixelMatrix[4]);
if(pix>Integer.MAX_VALUE) {
return Integer.MAX_VALUE;
}
if(pix<Integer.MIN_VALUE) {
return Integer.MIN_VALUE;
}
return pix;
}
/**
* Converts pixel to x world units.
* @param pix
* @return x panel units
*/
public double pixToY(int pix) {
return ymax-(pix-topGutter)/yPixPerUnit;
}
/**
* Converts y from world to pixel units.
* @param y
* @return the pixel value of the y coordinate
*/
public int yToPix(double y) {
// double pix = (ymax - y) * yPixPerUnit + topGutter;
double pix = pixelMatrix[3]*y+pixelMatrix[5];
if(pix>Integer.MAX_VALUE) {
return Integer.MAX_VALUE;
}
if(pix<Integer.MIN_VALUE) {
return Integer.MIN_VALUE;
}
// return (int)Math.round(pix);
return(int) Math.floor((float) pix); // gives better registration with affine transformation
}
/**
* Converts y from world to graphics device units.
* @param y
* @return the graphics device value of the x coordinate
*/
public float yToGraphics(double y) {
float pix = (float) (pixelMatrix[3]*y+pixelMatrix[5]);
if(pix>Integer.MAX_VALUE) {
return Integer.MAX_VALUE;
}
if(pix<Integer.MIN_VALUE) {
return Integer.MIN_VALUE;
}
return pix;
}
/**
* Sets axis scales if autoscale is true using the max and min values of the measurable objects.
*/
public void scale() {
ArrayList<Drawable> tempList = getDrawables(); // clone the list of drawables
scale(tempList);
}
/**
* Sets axis scales if autoscale is true using the max and min values of the objects in the given list.
*/
protected void scale(ArrayList<Drawable> tempList) {
if(autoscaleX) {
scaleX(tempList);
}
if(autoscaleY) {
scaleY(tempList);
}
}
/**
* Sets the scale based on the max and min values of all measurable objects.
*
* Autoscale flags are not respected.
*/
public void measure() {
ArrayList<Drawable> tempList = getDrawables(); // this clones the list of drawables
scaleX(tempList);
scaleY(tempList);
setPixelScale();
invalidateImage(); // validImage = false;
}
/**
* Sets the x axis scale based on the max and min values of all measurable objects. Autoscale flag is not respected.
*/
protected void scaleX() {
ArrayList<Drawable> tempList = getDrawables(); // this clones the list of drawables
scaleX(tempList);
}
/**
* Sets the x axis scale based on the max and/or min values of all measurable objects.
* Autoscale flag is not respected.
*/
protected void scaleX(ArrayList<Drawable> tempList) {
double newXMin = Double.MAX_VALUE;
double newXMax = -Double.MAX_VALUE;
boolean measurableFound = false;
Iterator<Drawable> it = tempList.iterator();
while(it.hasNext()) {
Object obj = it.next();
if(!(obj instanceof Measurable)) {
continue; // object is not measurable
}
Measurable measurable = (Measurable) obj;
if(!measurable.isMeasured()) {
continue; // objects' measure not yet set
}
double xmi = measurable.getXMin(), xma = measurable.getXMax();
if(logScaleX&&(measurable instanceof LogMeasurable)) {
xmi = ((LogMeasurable) measurable).getXMinLogscale();
xma = ((LogMeasurable) measurable).getXMaxLogscale();
}
if(!Double.isNaN(xmi)&&!Double.isNaN(xma)) {
newXMin = Math.min(newXMin, xmi);
newXMin = Math.min(newXMin, xma);
newXMax = Math.max(newXMax, xma);
newXMax = Math.max(newXMax, xmi);
measurableFound = true; // we have at least one valid min-max measure
}
}
// do not change change values unless there is at least one measurable object.
if(measurableFound) {
if(logScaleX&&((xLeftMarginPercentage>0.0)||(xRightMarginPercentage>0.0))) { // logscale
newXMax *= 1+xRightMarginPercentage/100.0;
newXMin /= 1+xLeftMarginPercentage/100.0;
}else if(!logScaleX&&((xLeftMarginPercentage>0.0)||(xRightMarginPercentage>0.0))) {
double xMed = (newXMin+newXMax)/2, xLen = (newXMax-newXMin)/2;
newXMax = xMed+xLen*(1.0+xRightMarginPercentage/100.0);
newXMin = xMed-xLen*(1.0+xLeftMarginPercentage/100.0);
}
if(newXMax-newXMin<Float.MIN_VALUE) { // range is too small to be meaningful for plotting; newXMax-newXMin is always positive
if(Double.isNaN(xfloor)) {
newXMin = 0.9*newXMin-0.5;
} else {
newXMin = Math.min(newXMin, xfloor);
}
if(Double.isNaN(xceil)) {
newXMax = 1.1*newXMax+0.5;
} else {
newXMax = Math.max(newXMax, xceil);
}
}
double range = Math.max(newXMax-newXMin,Float.MIN_VALUE); // range will always be positive
while(Math.abs((newXMax+range)/range)>1e5) { // limit autoscale to 5 decimal places
range *= 2; // increase the range
newXMin -= range;
newXMax += range;
}
if(autoscaleXMin) {
xminPreferred = newXMin-autoscaleMargin*range;
}
if(autoscaleXMax) {
xmaxPreferred = newXMax+autoscaleMargin*range;
}
} else { // we don't have any measurables
if(!Double.isNaN(xfloor)&&autoscaleXMin) {
xminPreferred = xfloor;
}
if(!Double.isNaN(xceil)&&autoscaleXMax) {
xmaxPreferred = xceil;
}
}
if(!Double.isNaN(xfloor)) {
xminPreferred = Math.min(xfloor, xminPreferred);
}
if(!Double.isNaN(xceil)) {
xmaxPreferred = Math.max(xceil, xmaxPreferred);
}
// final check to see if range is too small
if(Math.abs(xmaxPreferred-xminPreferred)<Float.MIN_VALUE) {
// center scale around xmaxPreferred
xminPreferred = 0.9*xmaxPreferred-Float.MIN_VALUE;
xmaxPreferred = 1.1*xmaxPreferred+Float.MIN_VALUE;
}
}
/**
* Sets the y axis scale based on the max and min values of all measurable objects. Autoscale flag is not respected.
*/
protected void scaleY() {
ArrayList<Drawable> tempList = getDrawables(); // this clones the list of drawables
scaleY(tempList);
}
/**
* Sets the y axis scale based on the max and min values of all measurable objects. Autoscale flag is not respected.
*/
protected void scaleY(ArrayList<Drawable> tempList) {
double newYMin = Double.MAX_VALUE;
double newYMax = -Double.MAX_VALUE;
boolean measurableFound = false;
Iterator<Drawable> it = tempList.iterator();
while(it.hasNext()) {
Object obj = it.next();
if(!(obj instanceof Measurable)) {
continue; // object is not measurable
}
Measurable measurable = (Measurable) obj;
if(!measurable.isMeasured()) {
continue; // objects' measure not yet set
}
double ymi = measurable.getYMin(), yma = measurable.getYMax();
//System.out.println("measurable="+measurable+" is Log Measurable?"+ (measurable instanceof LogMeasurable));
if(logScaleY&&(measurable instanceof LogMeasurable)) {
yma = ((LogMeasurable) measurable).getYMaxLogscale();
ymi = ((LogMeasurable) measurable).getYMinLogscale();
//System.out.println("ymin="+ymi+ " ymax="+yma+ " measurable="+measurable);
}
if(!Double.isNaN(ymi)&&!Double.isNaN(yma)) {
newYMin = Math.min(newYMin, ymi);
newYMin = Math.min(newYMin, yma);
newYMax = Math.max(newYMax, yma);
newYMax = Math.max(newYMax, ymi);
measurableFound = true;
}
}
// do not change change values unless there is at least one measurable object.
if(measurableFound) {
if(logScaleY&&((yTopMarginPercentage>0.0)||(yBottomMarginPercentage>0.0))) {
newYMax *= 1.0+yTopMarginPercentage/100.0;
newYMin /= 1.0+yBottomMarginPercentage/100.0;
}else if(!logScaleY&&((yTopMarginPercentage>0.0)||(yBottomMarginPercentage>0.0))) {
double yMed = (newYMin+newYMax)/2, yLen = (newYMax-newYMin)/2;
newYMax = yMed+yLen*(1.0+yTopMarginPercentage/100.0);
newYMin = yMed-yLen*(1.0+yBottomMarginPercentage/100.0);
}
if(newYMax-newYMin<Float.MIN_VALUE) {
if(Double.isNaN(yfloor)) {
newYMin = 0.9*newYMin-0.5;
} else {
newYMin = Math.min(newYMin, yfloor);
}
if(Double.isNaN(yceil)) {
newYMax = 1.1*newYMax+0.5;
} else {
newYMax = Math.max(newYMax, yceil);
}
}
double range = Math.max(newYMax-newYMin,Float.MIN_VALUE);
while(Math.abs((newYMax+range)/range)>1e5) { // limit autoscale to 5 decimal places
range *= 2; // increase the range
newYMin -= range;
newYMax += range;
}
if(autoscaleYMin) {
yminPreferred = newYMin-autoscaleMargin*range;
}
if(autoscaleYMax) {
ymaxPreferred = newYMax+autoscaleMargin*range;
}
} else { // we don't have any measurables
if(!Double.isNaN(yfloor)&&autoscaleYMin) {
yminPreferred = yfloor;
}
if(!Double.isNaN(yceil)&&autoscaleYMax) {
ymaxPreferred = yceil;
}
}
if(!Double.isNaN(yfloor)) {
yminPreferred = Math.min(yfloor, yminPreferred);
}
if(!Double.isNaN(yceil)) {
ymaxPreferred = Math.max(yceil, ymaxPreferred);
}
// final check for minimum separation
if(Math.abs(ymaxPreferred-yminPreferred)<Float.MIN_VALUE) {
// center scale around ymaxPreferred
yminPreferred = 0.9*ymaxPreferred-Float.MIN_VALUE;
ymaxPreferred = 1.1*ymaxPreferred+Float.MIN_VALUE;
}
}
/**
* Paints all the drawable objects in the panel.
* @param g
*/
protected void paintDrawableList(Graphics g, ArrayList<Drawable> tempList) {
if(tempList==null) {
return;
}
Graphics2D g2 = (Graphics2D) g;
Iterator<Drawable> it = tempList.iterator();
Shape clipShape = g2.getClip();
int w = getWidth()-leftGutter-rightGutter;
int h = getHeight()-bottomGutter-topGutter;
if((w<0)||(h<0)) {
return;
}
if(clipAtGutter) {
g2.clipRect(leftGutter, topGutter, w, h);
}
if(!tempList.isEmpty()&&(tempList.get(0) instanceof False3D)) {
tempList.get(0).draw(this, g2);
} else {
while(it.hasNext()) {
if(!validImage) {
break; // abort drawing
}
Drawable drawable = it.next();
drawable.draw(this, g2);
}
}
g2.setClip(clipShape);
}
/**
* Gets the glass panel.
*
* The glass panel is a trasparent panel that contians the messages boxes and other compotnents.
* @return JPanel
*/
public JPanel getGlassPanel() {
return glassPanel;
}
public void setIgnoreRepaint(boolean ignoreRepaint) {
super.setIgnoreRepaint(ignoreRepaint);
glassPanel.setIgnoreRepaint(ignoreRepaint);
}
/**
* Gets the object that sets the gutters for this panel.
* @return Dimensioned
*/
public Dimensioned getDimensionSetter() {
return dimensionSetter;
}
/**
* Adds a drawable object to the drawable list.
* @param drawable
*/
public void addDrawable(Drawable drawable) {
synchronized(drawableList) {
if((drawable!=null)&&!drawableList.contains(drawable)) {
drawableList.add(drawable);
invalidateImage(); // validImage = false;
}
}
if(drawable instanceof Dimensioned) {
dimensionSetter = ((Dimensioned) drawable);
}
}
/**
* Adds a collection of drawable objects to the drawable list.
* @param drawables
*/
public void addDrawables(Collection<Drawable> drawables) {
synchronized(drawableList) {
Iterator<Drawable> it = drawables.iterator();
while(it.hasNext()) {
Object obj = it.next();
if(obj instanceof Drawable) {
addDrawable((Drawable) obj);
}
}
}
}
/**
* Adds a drawable object to the drawable list at the given index.
* @param drawable
*/
public void addDrawableAtIndex(int index, Drawable drawable) {
synchronized(drawableList) {
if((drawable!=null)&&!drawableList.contains(drawable)) {
drawableList.add(index, drawable);
invalidateImage(); // validImage = false;
}
}
if(drawable instanceof Dimensioned) {
dimensionSetter = ((Dimensioned) drawable);
}
}
/**
* Replaces a drawable object with another drawable.
*
* @param oldDrawable Drawable
* @param newDrawable Drawable
*/
public void replaceDrawable(Drawable oldDrawable, Drawable newDrawable) {
synchronized(drawableList) {
if((oldDrawable!=null)&&drawableList.contains(oldDrawable)) {
int i = drawableList.indexOf(oldDrawable);
drawableList.set(i, newDrawable);
if(newDrawable instanceof Dimensioned) {
dimensionSetter = ((Dimensioned) newDrawable);
}
} else {
addDrawable(newDrawable); // oldDrawable does not exist
}
}
}
/**
* Removes a drawable object from the drawable list.
* @param drawable
*/
public void removeDrawable(Drawable drawable) {
synchronized(drawableList) {
drawableList.remove(drawable);
}
if(drawable instanceof Dimensioned) {
dimensionSetter = null;
}
}
/**
* Removes all objects of the given class from the drawable list.
*
* Assignable subclasses are NOT removed. Interfaces CANNOT be specified.
*
* @param c the class
* @see #removeDrawables(Class c)
*/
public <T extends Drawable> void removeObjectsOfClass(Class<T> c) {
synchronized(drawableList) {
Iterator<Drawable> it = drawableList.iterator();
while(it.hasNext()) {
Object element = it.next();
if(element.getClass()==c) {
it.remove();
if(element instanceof Dimensioned) {
dimensionSetter = null;
}
}
}
}
}
/**
* Removes all objects assignable to the given class from the drawable list.
* Interfaces can be specified.
*
* @param c the class
* @see #removeObjectsOfClass(Class c)
*/
public <T extends Drawable> void removeDrawables(Class<T> c) {
synchronized(drawableList) {
Iterator<Drawable> it = drawableList.iterator();
while(it.hasNext()) {
Object element = it.next();
if(c.isInstance(element)) {
it.remove();
if(element instanceof Dimensioned) {
dimensionSetter = null;
}
}
}
}
}
/**
* Removes the option controller.
*
* The option controller may interfere with other mouse actions
*/
public void removeOptionController() {
removeMouseListener(optionController);
removeMouseMotionListener(optionController);
}
/**
* Removes the option controller.
*
* The option controller may interfere with other mouse actions
*/
public void addOptionController() {
addMouseListener(optionController);
addMouseMotionListener(optionController);
}
/**
* Removes all drawable objects from the drawable list.
*/
public void clear() {
synchronized(drawableList) {
drawableList.clear();
}
dimensionSetter = null;
}
/**
* Gets the cloned list of Drawable objects.
*
* This is a shallow clone. The same objects will be in both the drawable list and the
* cloned list.
* @return cloned list
*/
public ArrayList<Drawable> getDrawables() {
synchronized(drawableList) {
return new ArrayList<Drawable>(drawableList);
}
}
/**
* Gets Drawable objects of an assignable type. The list contains
* objects that are assignable from the class or interface.
*
* Returns a shallow clone. The same objects will be in the drawable list and the
* cloned list.
*
* @param type the type of Drawable object
*
* @return the cloned list
*
* @see #getObjectOfClass(Class c)
*/
public <T extends Drawable> ArrayList<T> getDrawables(Class<T> type) {
ArrayList<Drawable> all = null;
synchronized(drawableList) {
all = new ArrayList<Drawable>(drawableList);
}
ArrayList<T> objects = new ArrayList<T>();
for(Drawable d : all) {
if(type.isInstance(d)) {
objects.add(type.cast(d));
}
}
return objects;
}
/**
* Gets objects of a specific class from the drawables list.
*
* Assignable subclasses are NOT returned. Interfaces CANNOT be specified.
* The same objects will be in the drawable list and the cloned list.
*
* @param type the class of the object
*
* @return the list
* @see #getDrawables(Class c)
*/
public <T extends Drawable> ArrayList<T> getObjectOfClass(Class<T> type) {
ArrayList<Drawable> all = null;
synchronized(drawableList) {
all = new ArrayList<Drawable>(drawableList);
}
ArrayList<T> objects = new ArrayList<T>();
for(Drawable d : all) {
if(d.getClass()==type) {
objects.add(type.cast(d));
}
}
return objects;
}
/**
* Gets the gutters.
*/
public int[] getGutters() {
return new int[] {leftGutter, topGutter, rightGutter, bottomGutter};
}
/**
* Sets the gutters using the given array.
* @param gutters int[]
*/
public void setGutters(int[] gutters) {
leftGutter = gutters[0];
topGutter = gutters[1];
rightGutter = gutters[2];
bottomGutter = gutters[3];
}
/**
* Sets gutters around the drawing area.
* @param left
* @param top
* @param right
* @param bottom
*/
public void setGutters(int left, int top, int right, int bottom) {
leftGutter = left;
topGutter = top;
rightGutter = right;
bottomGutter = bottom;
}
/**
* Sets preferred gutters around the drawing area.
* @param left
* @param top
* @param right
* @param bottom
*/
public void setPreferredGutters(int left, int top, int right, int bottom) {
leftGutterPreferred = leftGutter = left;
topGutterPreferred = topGutter = top;
rightGutterPreferred = rightGutter = right;
bottomGutterPreferred = bottomGutter = bottom;
}
/**
* Resets the gutters to their preferred values.
*/
public void resetGutters() {
leftGutter = leftGutterPreferred;
topGutter = topGutterPreferred;
rightGutter = rightGutterPreferred;
bottomGutter = bottomGutterPreferred;
}
/**
* Gets the bottom gutter of this DrawingPanel.
*
* @return bottom gutter
*/
public int getBottomGutter() {
return bottomGutter;
}
/**
* Gets the bottom gutter of this DrawingPanel.
*
* @return right gutter
*/
public int getTopGutter() {
return topGutter;
}
/**
* Gets the left gutter of this DrawingPanel.
*
* @return left gutter
*/
public int getLeftGutter() {
return leftGutter;
}
/**
* Gets the right gutter of this DrawingPanel.
*
* @return right gutter
*/
public int getRightGutter() {
return rightGutter;
}
/**
* Shows a message in a yellow text box in the lower right hand corner.
*
* @param msg
*/
public void setMessage(String msg) {
brMessageBox.setText(msg); // the default message box
}
/**
* Shows a message in a yellow text box.
*
* location 0=bottom left
* location 1=bottom right
* location 2=top right
* location 3=top left
*
* @param msg
* @param location
*/
public void setMessage(String msg, int location) {
switch(location) {
case 0 : // usually used for mouse coordiantes
blMessageBox.setText(msg);
break;
case 1 :
brMessageBox.setText(msg);
break;
case 2 :
trMessageBox.setText(msg);
break;
case 3 :
tlMessageBox.setText(msg);
break;
}
}
/**
* Show the coordinates in the text box in the lower left hand corner.
*
* @param show
*/
public void setShowCoordinates(boolean show) {
if(showCoordinates&&!show) {
this.removeMouseListener(mouseController);
this.removeMouseMotionListener(mouseController);
} else if(!showCoordinates&&show) {
this.addMouseListener(mouseController);
this.addMouseMotionListener(mouseController);
}
showCoordinates = show;
}
/**
* Returns true if an event starts or ends a zoom operation. Used by
* OptionController. Method added by D Brown 04 Nov 2011.
*
* @param e a mouse event
* @return true if a zoom event
*/
public boolean isZoomEvent(MouseEvent e) {
return OSPRuntime.isPopupTrigger(e);
}
/**
* The CMController class handles mouse related events in order to display
* coordinates in the mouse box.
*/
private class CMController extends MouseInputAdapter {
/**
* Handle the mouse pressed event.
* @param e
*/
public void mousePressed(MouseEvent e) {
String s = coordinateStrBuilder.getCoordinateString(DrawingPanel.this, e);
blMessageBox.setText(s);
}
/**
* Handle the mouse released event.
* @param e
*/
public void mouseReleased(MouseEvent e) {
blMessageBox.setText(null);
}
/**
* Handle the mouse entered event.
* @param e
*/
public void mouseEntered(MouseEvent e) {
if(showCoordinates) {
setMouseCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
}
}
/**
* Handle the mouse exited event.
* @param e
*/
public void mouseExited(MouseEvent e) {
setMouseCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
}
/**
* Handle the mouse dragged event.
* @param e
*/
public void mouseDragged(MouseEvent e) {
String s = coordinateStrBuilder.getCoordinateString(DrawingPanel.this, e);
blMessageBox.setText(s);
}
}
/**
* ZoomBox creates an on-screen rectangle using XORMode for fast redrawing.
*/
public class ZoomBox {
int xstart, ystart; // start of mouse down
int xstop, ystop; // most recent mouse drag
int xlast, ylast; // corner position during last drawing
boolean visible = false;
boolean dragged = false;
boolean showUndraggedBox = true;
/**
* Starts the zoom by saving the corner location.
*
* @param xpix
* @param ypix
*/
public void startZoom(int xpix, int ypix) {
if(!isZoom()) {
return;
}
visible = true;
dragged = false;
xlast = xstop = xstart = xpix;
ylast = ystop = ystart = ypix;
repaint();
}
/**
* Hides the zoom box.
*/
public void hide() {
visible = false;
repaint();
}
/**
* Sets the showUndraggedBox flag.
*
* @param show true to show a zoom box when the mouse is not dragged
*/
public void setShowUndraggedBox(boolean show) {
showUndraggedBox = show;
}
/**
* Drags the corner of the ZoomBox.
* Drag uses XORMode drawing to first erase and then repaint the box.
*
* @param xpix
* @param ypix
*/
public void drag(int xpix, int ypix) {
if(!visible) {
return;
}
dragged = true;
xstop = xpix;
ystop = ypix;
Graphics g = getGraphics();
if(g==null) {
return;
}
g.setXORMode(Color.green);
g.drawRect(Math.min(xstart, xlast), Math.min(ystart, ylast), Math.abs(xlast-xstart), Math.abs(ylast-ystart));
xlast = xstop;
ylast = ystop;
g.drawRect(Math.min(xstart, xlast), Math.min(ystart, ylast), Math.abs(xlast-xstart), Math.abs(ylast-ystart));
g.setPaintMode();
g.dispose();
}
/**
* Paints the ZoomBox after dragging is complete.
*
* @param g
*/
void paint(Graphics g) {
if(!visible) {
return;
}
if((xstop==xstart)||(ystop==ystart)) {
return;
}
g.setColor(Color.magenta);
g.drawRect(Math.min(xstart, xstop), Math.min(ystart, ystop), Math.abs(xstop-xstart), Math.abs(ystop-ystart));
}
/**
* Reports the drag status of the zoom box.
*
* @return true if the zoom box has been dragged
*/
public boolean isDragged() {
return dragged&&(xstop!=xstart)&&(ystop!=ystart);
}
/**
* Gets the visibility of the zoom box.
*
* @return true if visible
*/
public boolean isVisible() {
return visible;
}
/**
* Reports the zoom rectangle in pixel units.
*/
public Rectangle reportZoom() {
int xmin = Math.min(xstart, xstop);
int xmax = Math.max(xstart, xstop);
int ymin = Math.min(ystart, ystop);
int ymax = Math.max(ystart, ystop);
return new Rectangle(xmin, ymin, xmax-xmin, ymax-ymin);
}
}
class PopupmenuListener implements ActionListener {
public void actionPerformed(ActionEvent evt) {
zoomBox.visible = false;
repaint();
String cmd = evt.getActionCommand();
if(cmd.equals(DisplayRes.getString("DrawingFrame.InspectMenuItem"))) { //$NON-NLS-1$
showInspector();
} else if(cmd.equals(DisplayRes.getString("DisplayPanel.Snapshot_menu_item"))) { //$NON-NLS-1$
snapshot();
} else if(cmd.equals(DisplayRes.getString("DisplayPanel.Zoom_in_menu_item"))) { //$NON-NLS-1$
setAutoscaleX(false);
setAutoscaleY(false);
zoomIn(); // sets zoomMode to true
} else if(cmd.equals(DisplayRes.getString("DisplayPanel.Zoom_out_menu_item"))) { //$NON-NLS-1$
setAutoscaleX(false);
setAutoscaleY(false);
zoomOut();
} else if(cmd.equals(DisplayRes.getString("DrawingFrame.Autoscale_menu_item"))) { //$NON-NLS-1$
double nan = Double.NaN;
setPreferredMinMax(nan, nan, nan, nan);
} else if(cmd.equals(DisplayRes.getString("DrawingFrame.Scale_menu_item"))) { //$NON-NLS-1$
ScaleInspector plotInspector = new ScaleInspector(DrawingPanel.this);
plotInspector.setLocationRelativeTo(DrawingPanel.this);
plotInspector.updateDisplay();
FontSizer.setFonts(plotInspector, FontSizer.getLevel());
plotInspector.pack();
plotInspector.setVisible(true);
}
}
}
/**
* OptionController handles mouse actions including zoom.
*/
class OptionController extends MouseInputAdapter {
/**
* Handles the mouse pressed event.
* @param e
*/
public void mousePressed(MouseEvent e) {
if(isZoomEvent(e)) {
zoomBox.startZoom(e.getX(), e.getY());
} else {
zoomBox.visible = false;
repaint();
}
}
/**
* Handles the mouse dragged event.
* @param e
*/
public void mouseDragged(MouseEvent e) {
zoomBox.drag(e.getX(), e.getY());
}
/**
* Handles the mouse released event.
*
* @param e
*/
public void mouseReleased(MouseEvent e) {
if(isZoomEvent(e)&&(popupmenu!=null)&&popupmenu.isEnabled()) {
if(isZoom()&&!zoomBox.isDragged()&& zoomBox.showUndraggedBox) {
Dimension dim = viewRect==null? getSize(): viewRect.getSize();
dim.width -= getLeftGutter()+getRightGutter();
dim.height -= getTopGutter()+getBottomGutter();
zoomBox.xstart = e.getX()-dim.width/4;
zoomBox.xstop = e.getX()+dim.width/4;
zoomBox.ystart = e.getY()-dim.height/4;
zoomBox.ystop = e.getY()+dim.height/4;
zoomBox.visible = true;
repaint();
}
JPopupMenu popup = getPopupMenu();
if (popup!=null)
popup.show(e.getComponent(), e.getX(), e.getY());
return;
}
else if(OSPRuntime.isPopupTrigger(e)&&(popupmenu==null)&&(customInspector!=null)) {
customInspector.setVisible(true);
return;
}
}
/**
* Method mouseMoved
*
* @param e
*/
public void mouseMoved(MouseEvent e) {
KeyboardFocusManager focuser =
KeyboardFocusManager.getCurrentKeyboardFocusManager();
Component focusOwner = focuser.getFocusOwner();
if (focusOwner != null && !(focusOwner instanceof JTextComponent)) {
requestFocusInWindow();
}
}
}
class GlassPanel extends JPanel {
public void render(Graphics g) {
try {
Component[] c = glassPanelLayout.getComponents();
for(int i = 0, n = c.length; i<n; i++) {
if(c[i]==null) {
continue;
}
g.translate(c[i].getX(), c[i].getY());
c[i].print(g);
g.translate(-c[i].getX(), -c[i].getY());
}
} catch(Exception ex) {/* do nothing if drawing fails*/ }
}
}
/**
* Returns an XML.ObjectLoader to save and load object data.
*
* @return the XML.ObjectLoader
*/
public static XML.ObjectLoader getLoader() {
return new DrawingPanelLoader();
}
/**
* A class to save and load DrawingPanel data.
*/
static class DrawingPanelLoader implements XML.ObjectLoader {
/**
* Saves DrawingPanel data in an XMLControl.
*
* @param control the control
* @param obj the DrawingPanel to save
*/
public void saveObject(XMLControl control, Object obj) {
DrawingPanel panel = (DrawingPanel) obj;
control.setValue("preferred x min", panel.getPreferredXMin()); //$NON-NLS-1$
control.setValue("preferred x max", panel.getPreferredXMax()); //$NON-NLS-1$
control.setValue("preferred y min", panel.getPreferredYMin()); //$NON-NLS-1$
control.setValue("preferred y max", panel.getPreferredYMax()); //$NON-NLS-1$
control.setValue("autoscale x", panel.isAutoscaleX()); //$NON-NLS-1$
control.setValue("autoscale y", panel.isAutoscaleY()); //$NON-NLS-1$
control.setValue("square aspect", panel.isSquareAspect()); //$NON-NLS-1$
control.setValue("drawables", panel.getDrawables()); //$NON-NLS-1$
}
/**
* Creates a DrawingPanel.
*
* @param control the control
* @return the newly created panel
*/
public Object createObject(XMLControl control) {
DrawingPanel panel = new DrawingPanel();
double xmin = control.getDouble("preferred x min"); //$NON-NLS-1$
double xmax = control.getDouble("preferred x max"); //$NON-NLS-1$
double ymin = control.getDouble("preferred y min"); //$NON-NLS-1$
double ymax = control.getDouble("preferred y max"); //$NON-NLS-1$
panel.setPreferredMinMax(xmin, xmax, ymin, ymax); // this sets autoscale to false
if(control.getBoolean("autoscale x")) { //$NON-NLS-1$
panel.setAutoscaleX(true);
}
if(control.getBoolean("autoscale y")) { //$NON-NLS-1$
panel.setAutoscaleY(true);
}
return panel;
}
/**
* Loads a DrawingPanel with data from an XMLControl.
*
* @param control the control
* @param obj the object
* @return the loaded object
*/
public Object loadObject(XMLControl control, Object obj) {
DrawingPanel panel = (DrawingPanel) obj;
double xmin = control.getDouble("preferred x min"); //$NON-NLS-1$
double xmax = control.getDouble("preferred x max"); //$NON-NLS-1$
double ymin = control.getDouble("preferred y min"); //$NON-NLS-1$
double ymax = control.getDouble("preferred y max"); //$NON-NLS-1$
panel.setPreferredMinMax(xmin, xmax, ymin, ymax); // this sets autoscale to false
panel.squareAspect = control.getBoolean("square aspect"); //$NON-NLS-1$
if(control.getBoolean("autoscale x")) { //$NON-NLS-1$
panel.setAutoscaleX(true);
}
if(control.getBoolean("autoscale y")) { //$NON-NLS-1$
panel.setAutoscaleY(true);
}
// load the drawables
Collection<?> drawables = Collection.class.cast(control.getObject("drawables")); //$NON-NLS-1$
if(drawables!=null) {
panel.clear();
Iterator<?> it = drawables.iterator();
while(it.hasNext()) {
panel.addDrawable((Drawable) it.next());
}
}
return obj;
}
}
}
/*
* Open Source Physics software is free software; you can redistribute
* it and/or modify it under the terms of the GNU General Public License (GPL) as
* published by the Free Software Foundation; either version 2 of the License,
* or(at your option) any later version.
*
* Code that uses any portion of the code in the org.opensourcephysics package
* or any subpackage (subdirectory) of this package must must also be be released
* under the GNU GPL license.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston MA 02111-1307 USA
* or view the license online at http://www.gnu.org/copyleft/gpl.html
*
* Copyright (c) 2007 The Open Source Physics project
* http://www.opensourcephysics.org
*/