org.jdesktop.swingx.JXGraph Maven / Gradle / Ivy
/*
* Copyright 2006 Sun Microsystems, Inc., 4150 Network Circle,
* Santa Clara, California 95054, U.S.A. All rights reserved.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
package org.jdesktop.swingx;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Stroke;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.geom.GeneralPath;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.LinkedList;
import java.util.List;
import javax.swing.JComponent;
import javax.swing.Painter;
import org.jdesktop.beans.AbstractBean;
import org.jdesktop.beans.JavaBean;
// TODO: keyboard navigation
// TODO: honor clip rect with text painting
// TODO: let client change zoom multiplier
// TODO: improve text drawing when origin is not on a multiple of majorX/majorY
// TODO: programmatically zoom in and out (or expose ZOOM_MULTIPLIER)
/**
* JXGraph
provides a component which can display one or more
* plots on top of a graduated background (or grid.)
*
* User input
*
* To help analyze the plots, this component allows the user to pan the
* view by left-clicking and dragging the mouse around. Using the mouse wheel,
* the user is also able to zoom in and out. Clicking the middle button resets
* the view to its original position.
*
* All user input can be disabled by calling
* {@link #setInputEnabled(boolean)} and passing false. This does not prevent
* subclasses from registering their own event listeners, such as mouse or key
* listeners.
*
* Initializing the component and setting the view
*
* Whenever a new instance of this component is created, the grid boundaries,
* or view, must be defined. The view is comprised of several elements whose
* descriptions are the following:
*
*
* - minX: Minimum value initially displayed by the component on the
* X axis (horizontally.)
* - minY: Minimum value initially displayed by the component on the
* Y axis (vertically.)
* - maxX: Maximum value initially displayed by the component on the
* X axis (horizontally.)
* - maxY: Maximum value initially displayed by the component on the
* Y axis (vertically.)
* - originX: Origin on the X axis of the vertical axis.
* - originY: Origin on the Y axis of the horizontal axis.
* - majorX: Distance between two major vertical lines of the
* grid.
* - majorY: Distance between two major horizontal lines of the
* grid.
* - minCountX: Number of minor vertical lines between two major
* vertical lines in the grid.
* - minCountY: Number of minor horizontal lines between two major
* horizontal lines in the grid.
*
*
* View and origin
*
* The default constructor defines a view bounds by -1.0
and
* +1.0
on both axis, and centered on an origin at
* (0, 0)
.
*
* To simplify the API, the origin can be read and written with a
* Point2D
instance (see {@link #getOrigin()} and
* {@link #setOrigin(Point2D)}.)
*
* Likewise, the view can be read and written with a
* Rectangle2D
instance (see {@link #getView()} and
* {@link #setView(Rectangle2D)}.) In this case, you need not to define the
* maximum boundaries of the view. Instead, you need to set the origin of the
* rectangle as the minimum boundaries. The width and the height of the
* rectangle define the distance between the minimum and maximum boundaries. For
* instance, to set the view to minX=-1.0, maxX=1.0, minY=-1.0 and maxY=1.0 you
* can use the following rectangle:
*
* new Rectangle2D.Double(-1.0d, -1.0d, 2.0d, 2.0d);
*
* You can check the boundaries by calling Rectangle2D.getMaxX()
* and Rectangle2D.getMaxY()
once your rectangle has been
* created.
*
* Alternatively, you can set the view and the origin at the same time by
* calling the method {@link #setViewAndOrigin(Rectangle2D)}. Calling this
* method will set the origin so as to center it in the view defined by the
* rectangle.
*
* Grid lines
*
* By default, the component defines a spacing of 0.2 units between two
* major grid lines. It also defines 4 minor grid lines between two major
* grid lines. The spacing between major grid lines and the number of minor
* grid lines can be accessed through the getters {@link #getMajorX()},
* {@link #getMajorY()}, {@link #getMinorCountX()} and
* {@link #getMinorCountY()}.
*
* You can change the number of grid lines at runtime by calling the setters
* {@link #setMajorX(double)}, {@link #setMajorY(double)},
* {@link #setMinorCountX(int)} and {@link #setMinorCountY(int)}.
*
* Appearance
*
* Although it provides sensible defaults, this component lets you change
* its appearance in several ways. It is possible to modify the colors of the
* graph by calling the setters {@link #setAxisColor(Color)},
* {@link #setMajorGridColor(Color)} and {@link #setMinorGridColor(Color)}.
*
* You can also enable or disable given parts of the resulting graph by
* calling the following setters:
*
*
* - {@link #setAxisPainted(boolean)}: Defines whether the main axis (see
* {@link #getOrigin()}) is painted.
* - {@link #setBackgroundPainted(boolean)}: Defines whether the background
* is painted (see {@link #setBackground(Color)}.)
* - {@link #setGridPainted(boolean)}: Defines whether the grid is
* painted.
* - {@link #setTextPainted(boolean)}: Defines whether the axis labels are
* painted.
*
*
* Usage example
*
* The following code snippet creates a new graph centered on
* (0, 0)
, bound to the view [-1.0 1.0 -1.0 1.0]
, with
* a major grid line every 0.5 units and a minor grid line count of 5:
*
*
* Point2D origin = new Point2D.Double(0.0d, 0.0d);
* Rectangle2D view = new Rectangle2D.Double(-1.0d, 1.0d, 2.0d, 2.0d);
* JXGraph graph = new JXGraph(origin, view, 0.5d, 5, 0.5d, 5);
*
*
* Plots
*
* Definition
*
* A plot is defined by a mathematical transformation that, given a value on
* the graph's X axis, returns a value on the Y axis. The component draws the
* result by plotting a spot of color at the coordinates defined by
* (X, f(X))
where f()
is the aforementionned
* mathematical transformation. Given the following transformation:
*
*
* f(X) = X * 2.0
*
*
* For X=1.0
, the component will show a spot of color at the
* coordinates (1.0, 2.0)
.
*
* Creating a new plot
*
* Every plot drawn by the component must be a subclass of
* {@link JXGraph.Plot}. This abstract public class defines a single method to
* be implemented by its children:
*
*
* public double compute(double value)
*
*
* The previous example can be defined by a concrete
* JXGraph.Plot
as follow:
*
*
* class TwiceTheValuePlot extends JXGraph.Plot {
* public double compute(double value) {
* return value * 2.0d;
* }
* }
*
*
* Most of the time though, a plot requires supplementary parameters. For
* instance, let's define the X axis of your graph as the mass of an object. To
* compute the weight of the object given its mass, you need to use the
* acceleration of gravity (w=m*g
where g
is the
* acceleration.) To let the user modify this last parameter, to compute his
* weight at the surface of the moon for instance, you need to add a parameter
* to your plot.
*
* While JXGraph.Plot
does not give you an API for such a
* purpose, it does define an event dispatching API (see
* {@link JXGraph#firePropertyChange(String, double, double)}.) Whenever a
* plot is added to the graph, the component registers itself as a property
* listener of the plot. If you take care of firing events whenever the user
* changes a parameter of your plot, the graph will automatically update its
* display. While not mandatory, it is highly recommended to leverage this
* API.
*
* Adding and removing plots to and from the graph
*
* To add a plot to the graph, simply call the method
* {@link #addPlots(Color, JXGraph.Plot...)}. You can use it to add one or more
* plots at the same time and associate them with a color. This color is used
* when drawing the plots:
*
*
* JXGraph.Plot plot = new TwiceTheValuePlot();
* graph.addPlots(Color.BLUE, plot);
*
*
* These two lines will display our previously defined plot in blue on
* screen. Removing one or several plots is as simple as calling the method
* {@link #removePlots(JXGraph.Plot...)}. You can also remove all plots at once
* with {@link #removeAllPlots()}.
*
* Painting more information
*
* How to draw on the graph
*
* If you need to add more information on the graph you need to extend
* it and override the method {@link #paintExtra(Graphics2D)}. This
* method has a default empty implementation and is called after everything
* has been drawn. Its sole parameter is a reference to the component's drawing
* surface, as configured by {@link #setupGraphics(Graphics2D)}. By default, the
* setup method activates antialising but it can be overriden to change the
* drawing surface. (Translation, rotation, new rendering hints, etc.)
*
* Getting the right coordinates
*
* To properly draw on the graph you will need to perform a translation
* between the graph's coordinates and the screen's coordinates. The component
* defines 4 methods to assist you in this task:
*
*
* - {@link #xPixelToPosition(double)}: Converts a pixel coordinate on the
* X axis into a world coordinate.
* - {@link #xPositionToPixel(double)}: Converts a world coordinate on the
* X axis into a pixel coordinate.
* - {@link #yPixelToPosition(double)}: Converts a pixel coordinate on the
* Y axis into a world coordinate.
* - {@link #yPositionToPixel(double)}: Converts a world coordinate on the
* Y axis into a pixel coordinate.
*
*
* If you have defined a graph view centered on the origin
* (0, 0)
, the origin of the graph will be at the exact center of
* the screen. That means the world coordinates (0, 0)
are
* equivalent to the pixel coordinates (width / 2, height / 2)
.
* Thus, calling xPositionToPixel(0.0d)
would give you the same
* value as the expression getWidth() / 2.0d
.
*
* Converting from world coordinates to pixel coordinates is mostly used to
* draw the result of a mathematical transformation. Converting from pixel
* coordinates to world coordinates is mostly used to get the position in the
* world of a mouse event.
*
* @see JXGraph.Plot
* @author Romain Guy [email protected]
*/
@JavaBean
public class JXGraph extends JXPanel {
private static final long serialVersionUID = 839170848365319138L;
// stroke widths used to draw the main axis and the grid
// the main axis is slightly thicker
private static final float STROKE_AXIS = 1.2f;
private static final float STROKE_GRID = 1.0f;
// defines by how much the view is shrinked or expanded everytime the
// user zooms in or out
private static final float ZOOM_MULTIPLIER = 1.1f;
//listens to changes to plots and repaints the graph
private PropertyChangeListener plotChangeListener;
// default color of the graph (does not include plots colors)
private Color majorGridColor = Color.GRAY.brighter();
private Color minorGridColor = new Color(220, 220, 220);
private Color axisColor = Color.BLACK;
// the list of plots currently known and displayed by the graph
private List plots;
// view boundaries as defined by the user
private double minX;
private double maxX;
private double minY;
private double maxY;
// the default view is set when the view is manually changed by the client
// it is used to reset the view in resetView()
private Rectangle2D defaultView;
// coordinates of the major axis
private double originX;
private double originY;
// definition of the grid
// various default values are used when the view is reset
private double majorX;
private double defaultMajorX;
private int minorCountX;
private double majorY;
private double defaultMajorY;
private int minorCountY;
// enables painting layers
private boolean textPainted = true;
private boolean gridPainted = true;
private boolean axisPainted = true;
private boolean backPainted = true;
// used by the PanHandler to move the view
private Point dragStart;
// mainFormatter is used for numbers > 0.01 and < 100
// secondFormatter uses scientific notation
private NumberFormat mainFormatter;
private NumberFormat secondFormatter;
// input handlers
private boolean inputEnabled = true;
private ZoomHandler zoomHandler;
private PanMotionHandler panMotionHandler;
private PanHandler panHandler;
private ResetHandler resetHandler;
/**
* Creates a new graph display. The following properties are
* automatically set:
*
* - view: -1.0 to +1.0 on both axis
* - origin: At
(0, 0)
* - grid: Spacing of 0.2 between major lines; minor lines
* count is 4
*
*/
public JXGraph() {
this(0.0, 0.0, -1.0, 1.0, -1.0, 1.0, 0.2, 4, 0.2, 4);
}
/**
* Creates a new graph display with the specified view.
* The following properties are automatically set:
*
* - origin: Center of the specified view
* - grid: Spacing of 0.2 between major lines; minor lines count is 4
*
*
* @param view the rectangle defining the view boundaries
*/
public JXGraph(Rectangle2D view) {
this(new Point2D.Double(view.getCenterX(), view.getCenterY()),
view, 0.2, 4, 0.2, 4);
}
/**
* Creates a new graph display with the specified view and grid lines.
* The origin is set at the center of the view.
*
* @param view the rectangle defining the view boundaries
* @param majorX the spacing between two major grid lines on the X axis
* @param minorCountX the number of minor grid lines between two major
* grid lines on the X axis
* @param majorY the spacing between two major grid lines on the Y axis
* @param minorCountY the number of minor grid lines between two major
* grid lines on the Y axis
* @throws IllegalArgumentException if minX ≥ maxX or minY ≥ maxY or
* minorCountX < 0 or minorCountY < 0 or
* majorX ≤ 0.0 or majorY ≤ 0.0
*/
public JXGraph(Rectangle2D view,
double majorX, int minorCountX,
double majorY, int minorCountY) {
this(new Point2D.Double(view.getCenterX(), view.getCenterY()),
view, majorX, minorCountX, majorY, minorCountY);
}
/**
* Creates a new graph display with the specified view and origin.
* The following properties are automatically set:
*
* - grid: Spacing of 0.2 between major lines; minor lines
* count is 4
*
*
* @param origin the coordinates of the main axis origin
* @param view the rectangle defining the view boundaries
*/
public JXGraph(Point2D origin, Rectangle2D view) {
this(origin, view, 0.2, 4, 0.2, 4);
}
/**
* Creates a new graph display with the specified view, origin and grid
* lines.
*
* @param origin the coordinates of the main axis origin
* @param view the rectangle defining the view boundaries
* @param majorX the spacing between two major grid lines on the X axis
* @param minorCountX the number of minor grid lines between two major
* grid lines on the X axis
* @param majorY the spacing between two major grid lines on the Y axis
* @param minorCountY the number of minor grid lines between two major
* grid lines on the Y axis
* @throws IllegalArgumentException
* if minX ≥ maxX or minY ≥ maxY or minorCountX < 0 or minorCountY < 0 or majorX ≤ 0.0 or majorY ≤ 0.0
*/
public JXGraph(Point2D origin, Rectangle2D view,
double majorX, int minorCountX,
double majorY, int minorCountY) {
this(origin.getX(), origin.getY(),
view.getMinX(), view.getMaxX(), view.getMinY(), view.getMaxY(),
majorX, minorCountX, majorY, minorCountY);
}
/**
* Creates a new graph display with the specified view, origin and grid
* lines.
*
* @param originX the coordinate of the major X axis
* @param originY the coordinate of the major Y axis
* @param minX the minimum coordinate on the X axis for the view
* @param maxX the maximum coordinate on the X axis for the view
* @param minY the minimum coordinate on the Y axis for the view
* @param maxY the maximum coordinate on the Y axis for the view
* @param majorX the spacing between two major grid lines on the X axis
* @param minorCountX the number of minor grid lines between two major
* grid lines on the X axis
* @param majorY the spacing between two major grid lines on the Y axis
* @param minorCountY the number of minor grid lines between two major
* grid lines on the Y axis
* @throws IllegalArgumentException
* if minX ≥ maxX or minY ≥ maxY or minorCountX < 0 or minorCountY < 0 or majorX ≤ 0.0 or majorY ≤ 0.0
*/
public JXGraph(double originX, double originY,
double minX, double maxX,
double minY, double maxY,
double majorX, int minorCountX,
double majorY, int minorCountY) {
if (minX >= maxX) {
throw new IllegalArgumentException("minX must be < to maxX");
}
if (minY >= maxY) {
throw new IllegalArgumentException("minY must be < to maxY");
}
if (minorCountX < 0) {
throw new IllegalArgumentException("minorCountX must be >= 0");
}
if (minorCountY < 0) {
throw new IllegalArgumentException("minorCountY must be >= 0");
}
if (majorX <= 0.0) {
throw new IllegalArgumentException("majorX must be > 0.0");
}
if (majorY <= 0.0) {
throw new IllegalArgumentException("majorY must be > 0.0");
}
this.originX = originX;
this.originY = originY;
this.minX = minX;
this.maxX = maxX;
this.minY = minY;
this.maxY = maxY;
this.defaultView = new Rectangle2D.Double(minX, minY,
maxX - minX, maxY - minY);
this.setMajorX(this.defaultMajorX = majorX);
this.setMinorCountX(minorCountX);
this.setMajorY(this.defaultMajorY = majorY);
this.setMinorCountY(minorCountY);
this.plots = new LinkedList();
this.mainFormatter = NumberFormat.getInstance();
this.mainFormatter.setMaximumFractionDigits(2);
this.secondFormatter = new DecimalFormat("0.##E0");
resetHandler = new ResetHandler();
addMouseListener(resetHandler);
panHandler = new PanHandler();
addMouseListener(panHandler);
panMotionHandler = new PanMotionHandler();
addMouseMotionListener(panMotionHandler);
zoomHandler = new ZoomHandler();
addMouseWheelListener(zoomHandler);
setBackground(Color.WHITE);
setForeground(Color.BLACK);
plotChangeListener = new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
repaint();
}
};
}
/**
* {@inheritDoc}
*/
@Override
public boolean isOpaque() {
if (!isBackgroundPainted()) {
return false;
}
return super.isOpaque();
}
/**
* {@inheritDoc}
* @see #setInputEnabled(boolean)
*/
@Override
public void setEnabled(boolean enabled) {
super.setEnabled(enabled);
setInputEnabled(enabled);
}
/**
* Enables or disables user input on the component. When user input is
* enabled, panning, zooming and view resetting. Disabling input will
* prevent the user from modifying the currently displayed view.
* Calling {@link #setEnabled(boolean)} disables the component in the
* Swing hierarchy and invokes this method.
*
* @param enabled true if user input must be enabled, false otherwise
* @see #setEnabled(boolean)
* @see #isInputEnabled()
*/
public void setInputEnabled(boolean enabled) {
if (inputEnabled != enabled) {
boolean old = isInputEnabled();
this.inputEnabled = enabled;
if (enabled) {
addMouseListener(resetHandler);
addMouseListener(panHandler);
addMouseMotionListener(panMotionHandler);
addMouseWheelListener(zoomHandler);
} else {
removeMouseListener(resetHandler);
removeMouseListener(panHandler);
removeMouseMotionListener(panMotionHandler);
removeMouseWheelListener(zoomHandler);
}
firePropertyChange("inputEnabled", old, isInputEnabled());
}
}
/**
*
Defines whether or not user input is accepted and managed by this
* component. The component is always created with user input enabled.
*
* @return true if user input is enabled, false otherwise
* @see #setInputEnabled(boolean)
*/
public boolean isInputEnabled() {
return inputEnabled;
}
/**
* Defines whether or not axis labels are painted by this component.
* The component is always created with text painting enabled.
*
* @return true if axis labels are painted, false otherwise
* @see #setTextPainted(boolean)
* @see #getForeground()
*/
public boolean isTextPainted() {
return textPainted;
}
/**
* Enables or disables the painting of axis labels depending on the
* value of the parameter. Text painting is enabled by default.
*
* @param textPainted if true, axis labels are painted
* @see #isTextPainted()
* @see #setForeground(Color)
*/
public void setTextPainted(boolean textPainted) {
boolean old = isTextPainted();
this.textPainted = textPainted;
firePropertyChange("textPainted", old, this.textPainted);
}
/**
* Defines whether or not grids lines are painted by this component.
* The component is always created with grid lines painting enabled.
*
* @return true if grid lines are painted, false otherwise
* @see #setGridPainted(boolean)
* @see #getMajorGridColor()
* @see #getMinorGridColor()
*/
public boolean isGridPainted() {
return gridPainted;
}
/**
* Enables or disables the painting of grid lines depending on the
* value of the parameter. Grid painting is enabled by default.
*
* @param gridPainted if true, axis labels are painted
* @see #isGridPainted()
* @see #setMajorGridColor(Color)
* @see #setMinorGridColor(Color)
*/
public void setGridPainted(boolean gridPainted) {
boolean old = isGridPainted();
this.gridPainted = gridPainted;
firePropertyChange("gridPainted", old, isGridPainted());
}
/**
* Defines whether or not the graph main axis is painted by this
* component. The component is always created with main axis painting
* enabled.
*
* @return true if main axis is painted, false otherwise
* @see #setTextPainted(boolean)
* @see #getAxisColor()
*/
public boolean isAxisPainted() {
return axisPainted;
}
/**
* Enables or disables the painting of main axis depending on the
* value of the parameter. Axis painting is enabled by default.
*
* @param axisPainted if true, axis labels are painted
* @see #isAxisPainted()
* @see #setAxisColor(Color)
*/
public void setAxisPainted(boolean axisPainted) {
boolean old = isAxisPainted();
this.axisPainted = axisPainted;
firePropertyChange("axisPainted", old, isAxisPainted());
}
/**
* Defines whether or not the background painted by this component.
* The component is always created with background painting enabled.
* When background painting is disabled, background painting is deferred
* to the parent class.
*
* @return true if background is painted, false otherwise
* @see #setBackgroundPainted(boolean)
* @see #getBackground()
*/
public boolean isBackgroundPainted() {
return backPainted;
}
/**
* Enables or disables the painting of background depending on the
* value of the parameter. Background painting is enabled by default.
*
* @param backPainted if true, axis labels are painted
* @see #isBackgroundPainted()
* @see #setBackground(Color)
*/
public void setBackgroundPainted(boolean backPainted) {
boolean old = isBackgroundPainted();
this.backPainted = backPainted;
firePropertyChange("backgroundPainted", old, isBackgroundPainted());
}
/**
* Gets the major grid lines color of this component.
*
* @return this component's major grid lines color
* @see #setMajorGridColor(Color)
* @see #setGridPainted(boolean)
*/
public Color getMajorGridColor() {
return majorGridColor;
}
/**
* Sets the color of major grid lines on this component. The color
* can be translucent.
*
* @param majorGridColor the color to become this component's major grid
* lines color
* @throws IllegalArgumentException if the specified color is null
* @see #getMajorGridColor()
* @see #isGridPainted()
*/
public void setMajorGridColor(Color majorGridColor) {
if (majorGridColor == null) {
throw new IllegalArgumentException("Color cannot be null.");
}
Color old = getMajorGridColor();
this.majorGridColor = majorGridColor;
firePropertyChange("majorGridColor", old, getMajorGridColor());
}
/**
* Gets the minor grid lines color of this component.
*
* @return this component's minor grid lines color
* @see #setMinorGridColor(Color)
* @see #setGridPainted(boolean)
*/
public Color getMinorGridColor() {
return minorGridColor;
}
/**
* Sets the color of minor grid lines on this component. The color
* can be translucent.
*
* @param minorGridColor the color to become this component's minor grid
* lines color
* @throws IllegalArgumentException if the specified color is null
* @see #getMinorGridColor()
* @see #isGridPainted()
*/
public void setMinorGridColor(Color minorGridColor) {
if (minorGridColor == null) {
throw new IllegalArgumentException("Color cannot be null.");
}
Color old = getMinorGridColor();
this.minorGridColor = minorGridColor;
firePropertyChange("minorGridColor", old, getMinorGridColor());
}
/**
* Gets the main axis color of this component.
*
* @return this component's main axis color
* @see #setAxisColor(Color)
* @see #setGridPainted(boolean)
*/
public Color getAxisColor() {
return axisColor;
}
/**
* Sets the color of main axis on this component. The color
* can be translucent.
*
* @param axisColor the color to become this component's main axis color
* @throws IllegalArgumentException if the specified color is null
* @see #getAxisColor()
* @see #isAxisPainted()
*/
public void setAxisColor(Color axisColor) {
if (axisColor == null) {
throw new IllegalArgumentException("Color cannot be null.");
}
Color old = getAxisColor();
this.axisColor = axisColor;
firePropertyChange("axisColor", old, getAxisColor());
}
/**
* Gets the distance, in graph units, between two major grid lines on
* the X axis.
*
* @return the spacing between two major grid lines on the X axis
* @see #setMajorX(double)
* @see #getMajorY()
* @see #setMajorY(double)
* @see #getMinorCountX()
* @see #setMinorCountX(int)
*/
public double getMajorX() {
return majorX;
}
/**
* Sets the distance, in graph units, between two major grid lines on the X axis.
*
* @param majorX the requested spacing between two major grid lines on the X axis
* @throws IllegalArgumentException if majorX is ≤ 0.0d
* @see #getMajorX()
* @see #getMajorY()
* @see #setMajorY(double)
* @see #getMinorCountX()
* @see #setMinorCountX(int)
*/
public void setMajorX(double majorX) {
if (majorX <= 0.0) {
throw new IllegalArgumentException("majorX must be > 0.0");
}
double old = getMajorX();
this.majorX = majorX;
this.defaultMajorX = majorX;
repaint();
firePropertyChange("majorX", old, getMajorX());
}
/**
* Gets the number of minor grid lines between two major grid lines
* on the X axis.
*
* @return the number of minor grid lines between two major grid lines
* @see #setMinorCountX(int)
* @see #getMinorCountY()
* @see #setMinorCountY(int)
* @see #getMajorX()
* @see #setMajorX(double)
*/
public int getMinorCountX() {
return minorCountX;
}
/**
* Sets the number of minor grid lines between two major grid lines on the X axis.
*
* @param minorCountX the number of minor grid lines between two major grid lines on the X axis
* @throws IllegalArgumentException if minorCountX is < 0
* @see #getMinorCountX()
* @see #getMinorCountY()
* @see #setMinorCountY(int)
* @see #getMajorX()
* @see #setMajorX(double)
*/
public void setMinorCountX(int minorCountX) {
if (minorCountX < 0) {
throw new IllegalArgumentException("minorCountX must be >= 0");
}
int old = getMinorCountX();
this.minorCountX = minorCountX;
repaint();
firePropertyChange("minorCountX", old, getMinorCountX());
}
/**
* Gets the distance, in graph units, between two major grid lines on
* the Y axis.
*
* @return the spacing between two major grid lines on the Y axis
* @see #setMajorY(double)
* @see #getMajorX()
* @see #setMajorX(double)
* @see #getMinorCountY()
* @see #setMinorCountY(int)
*/
public double getMajorY() {
return majorY;
}
/**
* Sets the distance, in graph units, between two major grid lines on the Y axis.
*
* @param majorY the requested spacing between two major grid lines on the Y axis
* @throws IllegalArgumentException if majorY is ≤ 0.0d
* @see #getMajorY()
* @see #getMajorX()
* @see #setMajorX(double)
* @see #getMinorCountY()
* @see #setMinorCountY(int)
*/
public void setMajorY(double majorY) {
if (majorY <= 0.0) {
throw new IllegalArgumentException("majorY must be > 0.0");
}
double old = getMajorY();
this.majorY = majorY;
this.defaultMajorY = majorY;
repaint();
firePropertyChange("majorY", old, getMajorY());
}
/**
* Gets the number of minor grid lines between two major grid lines
* on the Y axis.
*
* @return the number of minor grid lines between two major grid lines
* @see #setMinorCountY(int)
* @see #getMinorCountX()
* @see #setMinorCountX(int)
* @see #getMajorY()
* @see #setMajorY(double)
*/
public int getMinorCountY() {
return minorCountY;
}
/**
* Sets the number of minor grid lines between two major grid lines on
* the Y axis.
*
* @param minorCountY the number of minor grid lines between two major grid
* lines on the Y axis
* @throws IllegalArgumentException if minorCountY is < 0
* @see #getMinorCountY()
* @see #getMinorCountX()
* @see #setMinorCountX(int)
* @see #getMajorY()
* @see #setMajorY(double)
*/
public void setMinorCountY(int minorCountY) {
if (minorCountY < 0) {
throw new IllegalArgumentException("minorCountY must be >= 0");
}
int old = getMinorCountY();
this.minorCountY = minorCountY;
repaint();
firePropertyChange("minorCountY", old, getMinorCountY());
}
/**
* Sets the view and the origin of the graph at the same time. The view
* minimum boundaries are defined by the location of the rectangle passed
* as parameter. The width and height of the rectangle define the distance
* between the minimum and maximum boundaries:
*
*
* - minX: bounds.getX()
* - minY: bounds.getY()
* - maxY: bounds.getMaxX() (minX + bounds.getWidth())
* - maxX: bounds.getMaxY() (minY + bounds.getHeight())
*
*
* The origin is located at the center of the view. Its coordinates are
* defined by calling bounds.getCenterX() and bounds.getCenterY().
*
* @param bounds the rectangle defining the graph's view and its origin
* @see #getView()
* @see #setView(Rectangle2D)
* @see #getOrigin()
* @see #setOrigin(Point2D)
*/
public void setViewAndOrigin(Rectangle2D bounds) {
setView(bounds);
setOrigin(new Point2D.Double(bounds.getCenterX(), bounds.getCenterY()));
}
/**
* Sets the view of the graph. The view minimum boundaries are defined by
* the location of the rectangle passed as parameter. The width and height
* of the rectangle define the distance between the minimum and maximum
* boundaries:
*
*
* - minX: bounds.getX()
* - minY: bounds.getY()
* - maxY: bounds.getMaxX() (minX + bounds.getWidth())
* - maxX: bounds.getMaxY() (minY + bounds.getHeight())
*
*
* If the specified view is null, nothing happens.
*
* Calling this method leaves the origin intact.
*
* @param bounds the rectangle defining the graph's view and its origin
* @see #getView()
* @see #setViewAndOrigin(Rectangle2D)
*/
public void setView(Rectangle2D bounds) {
if (bounds == null) {
return;
}
Rectangle2D old = getView();
defaultView = new Rectangle2D.Double(bounds.getX(), bounds.getY(),
bounds.getWidth(), bounds.getHeight());
minX = defaultView.getMinX();
maxX = defaultView.getMaxX();
minY = defaultView.getMinY();
maxY = defaultView.getMaxY();
majorX = defaultMajorX;
majorY = defaultMajorY;
firePropertyChange("view", old, getView());
repaint();
}
/**
* Gets the view of the graph. The returned rectangle defines the bounds
* of the view as follows:
*
*
* - minX: bounds.getX()
* - minY: bounds.getY()
* - maxY: bounds.getMaxX() (minX + bounds.getWidth())
* - maxX: bounds.getMaxY() (minY + bounds.getHeight())
*
*
* @return the rectangle corresponding to the current view of the graph
* @see #setView(Rectangle2D)
* @see #setViewAndOrigin(Rectangle2D)
*/
public Rectangle2D getView() {
return new Rectangle2D.Double(minX, minY, maxX - minX, maxY - minY);
}
/**
* Resets the view to the default view if it has been changed by the user
* by panning and zooming. The default view is defined by the view last
* specified in a constructor call or a call to the methods
* {@link #setView(Rectangle2D)} and
* {@link #setViewAndOrigin(Rectangle2D)}.
*
* @see #setView(Rectangle2D)
* @see #setViewAndOrigin(Rectangle2D)
*/
public void resetView() {
setView(defaultView);
}
/**
* Sets the origin of the graph. The coordinates of the origin are
* defined by the coordinates of the point passed as parameter.
*
* If the specified view is null, nothing happens.
*
* Calling this method leaves the view intact.
*
* @param origin the coordinates of the new origin
* @see #getOrigin()
* @see #setViewAndOrigin(Rectangle2D)
*/
public void setOrigin(Point2D origin) {
if (origin == null) {
return;
}
Point2D old = getOrigin();
originX = origin.getX();
originY = origin.getY();
firePropertyChange("origin", old, getOrigin());
repaint();
}
/**
* Gets the origin coordinates of the graph. The coordinates are
* represented as an instance of Point2D
and stored in
* double
format.
* @return the origin coordinates in double format
* @see #setOrigin(Point2D)
* @see #setViewAndOrigin(Rectangle2D)
*/
public Point2D getOrigin() {
return new Point2D.Double(originX, originY);
}
/**
* Adds one or more plots to the graph. These plots are associated to
* a color used to draw them.
*
* If plotList is null or empty, nothing happens.
*
* This method is not thread safe and should be called only from the
* EDT.
*
* @param color the color to be usd to draw the plots
* @param plotList the list of plots to add to the graph
* @throws IllegalArgumentException if color is null
* @see #removePlots(JXGraph.Plot...)
* @see #removeAllPlots()
*/
public void addPlots(Color color, Plot... plotList) {
if (color == null) {
throw new IllegalArgumentException("Plots color cannot be null.");
}
if (plotList == null) {
return;
}
for (Plot plot : plotList) {
DrawablePlot drawablePlot =
new DrawablePlot(plot, color);
if (plot != null && !plots.contains(drawablePlot)) {
plot.addPropertyChangeListener(plotChangeListener);
plots.add(drawablePlot);
}
}
repaint();
}
/**
* Removes the specified plots from the graph. Plots to be removed
* are identified by identity. This means you cannot remove a plot by
* passing a clone or another instance of the same subclass of
* {@link JXGraph.Plot}.
*
* If plotList is null or empty, nothing happens.
*
* This method is not thread safe and should be called only from the
* EDT.
*
* @param plotList the list of plots to be removed from the graph
* @see #removeAllPlots()
* @see #addPlots(Color, JXGraph.Plot...)
*/
public void removePlots(Plot... plotList) {
if (plotList == null) {
return;
}
for (Plot plot : plotList) {
if (plot != null) {
DrawablePlot toRemove = null;
for (DrawablePlot drawable: plots) {
if (drawable.getEquation() == plot) {
toRemove = drawable;
break;
}
}
if (toRemove != null) {
plot.removePropertyChangeListener(plotChangeListener);
plots.remove(toRemove);
}
}
}
repaint();
}
/**
* Removes all the plots currently associated with this graph.
*
* This method is not thread safe and should be called only from the
* EDT.
*
* @see #removePlots(JXGraph.Plot...)
* @see #addPlots(Color, JXGraph.Plot...)
*/
public void removeAllPlots() {
plots.clear();
repaint();
}
/**
* {@inheritDoc}
*/
@Override
public Dimension getPreferredSize() {
return new Dimension(400, 400);
}
/**
* Converts a position, in graph units, from the Y axis into a pixel
* coordinate. For instance, if you defined the origin so it appears at the
* exact center of the view, calling
* yPositionToPixel(getOriginY())
will return a value
* approximately equal to getHeight() / 2.0
.
*
* @param position the Y position to be converted into pixels
* @return the coordinate in pixels of the specified graph Y position
* @see #xPositionToPixel(double)
* @see #yPixelToPosition(double)
*/
protected double yPositionToPixel(double position) {
double height = getHeight();
return height - ((position - minY) * height / (maxY - minY));
}
/**
* Converts a position, in graph units, from the X axis into a pixel
* coordinate. For instance, if you defined the origin so it appears at the
* exact center of the view, calling
* xPositionToPixel(getOriginX())
will return a value
* approximately equal to getWidth() / 2.0
.
*
* @param position the X position to be converted into pixels
* @return the coordinate in pixels of the specified graph X position
* @see #yPositionToPixel(double)
* @see #xPixelToPosition(double)
*/
protected double xPositionToPixel(double position) {
return (position - minX) * getWidth() / (maxX - minX);
}
/**
* Converts a pixel coordinate from the X axis into a graph position, in
* graph units. For instance, if you defined the origin so it appears at the
* exact center of the view, calling
* xPixelToPosition(getWidth() / 2.0)
will return a value
* approximately equal to getOriginX()
.
*
* @param pixel the X pixel coordinate to be converted into a graph position
* @return the graph X position of the specified pixel coordinate
* @see #yPixelToPosition(double)
* @see #xPositionToPixel(double)
*/
protected double xPixelToPosition(double pixel) {
// double axisV = xPositionToPixel(originX);
// return (pixel - axisV) * (maxX - minX) / (double) getWidth();
return minX + pixel * (maxX - minX) / getWidth();
}
/**
* Converts a pixel coordinate from the Y axis into a graph position, in
* graph units. For instance, if you defined the origin so it appears at the
* exact center of the view, calling
* yPixelToPosition(getHeight() / 2.0)
will return a value
* approximately equal to getOriginY()
.
*
* @param pixel the Y pixel coordinate to be converted into a graph position
* @return the graph Y position of the specified pixel coordinate
* @see #xPixelToPosition(double)
* @see #yPositionToPixel(double)
*/
protected double yPixelToPosition(double pixel) {
// double axisH = yPositionToPixel(originY);
// return (getHeight() - pixel - axisH) * (maxY - minY) / (double) getHeight();
return minY + (getHeight() - pixel) * (maxY - minY) / getHeight();
}
/**
* {@inheritDoc}
*/
@Override
protected void paintComponent(Graphics g) {
if (!isVisible()) {
return;
}
Graphics2D g2 = (Graphics2D) g;
setupGraphics(g2);
paintBackground(g2);
drawGrid(g2);
drawAxis(g2);
drawPlots(g2);
drawLabels(g2);
paintExtra(g2);
}
/**
* This painting method is meant to be overridden by subclasses of
* JXGraph
. This method is called after all the painting
* is done. By overriding this method, a subclass can display extra
* information on top of the graph.
* The graphics surface passed as parameter is configured by
* {@link #setupGraphics(Graphics2D)}.
*
* @param g2 the graphics surface on which the graph is drawn
* @see #setupGraphics(Graphics2D)
* @see #xPixelToPosition(double)
* @see #yPixelToPosition(double)
* @see #xPositionToPixel(double)
* @see #yPositionToPixel(double)
*/
protected void paintExtra(Graphics2D g2) {
}
// Draw all the registered plots with the appropriate color.
private void drawPlots(Graphics2D g2) {
for (DrawablePlot drawable: plots) {
g2.setColor(drawable.getColor());
drawPlot(g2, drawable.getEquation());
}
}
// Draw a single plot as a GeneralPath made of straight lines.
private void drawPlot(Graphics2D g2, Plot equation) {
float x = 0.0f;
float y = (float) yPositionToPixel(equation.compute(xPixelToPosition(0.0)));
GeneralPath path = new GeneralPath();
path.moveTo(x, y);
float width = getWidth();
for (x = 0.0f; x < width; x += 1.0f) {
double position = xPixelToPosition(x);
y = (float) yPositionToPixel(equation.compute(position));
path.lineTo(x, y);
}
g2.draw(path);
}
// Draws the grid. First draw the vertical lines, then the horizontal lines.
private void drawGrid(Graphics2D g2) {
Stroke stroke = g2.getStroke();
if (isGridPainted()) {
drawVerticalGrid(g2);
drawHorizontalGrid(g2);
}
g2.setStroke(stroke);
}
// Draw all labels. First draws labels on the horizontal axis, then labels
// on the vertical axis. If the axis is set not to be painted, this
// method draws the origin as a straight cross.
private void drawLabels(Graphics2D g2) {
if (isTextPainted()) {
double axisH = yPositionToPixel(originY);
double axisV = xPositionToPixel(originX);
if (isAxisPainted()) {
Stroke stroke = g2.getStroke();
g2.setStroke(new BasicStroke(STROKE_AXIS));
g2.setColor(getAxisColor());
g2.drawLine((int) axisV - 3, (int) axisH,
(int) axisV + 3, (int) axisH);
g2.drawLine((int) axisV, (int) axisH - 3,
(int) axisV, (int) axisH + 3);
g2.setStroke(stroke);
}
g2.setColor(getForeground());
FontMetrics metrics = g2.getFontMetrics();
g2.drawString(format(originX) + "; " +
format(originY), (int) axisV + 5,
(int) axisH + metrics.getHeight());
drawHorizontalAxisLabels(g2);
drawVerticalAxisLabels(g2);
}
}
// Draws labels on the vertical axis. First draws labels below the origin,
// then draw labels on top of the origin.
private void drawVerticalAxisLabels(Graphics2D g2) {
double axisV = xPositionToPixel(originX);
// double startY = Math.floor((minY - originY) / majorY) * majorY;
double startY = Math.floor(minY / majorY) * majorY;
for (double y = startY; y < maxY + majorY; y += majorY) {
if (((y - majorY / 2.0) < originY) &&
((y + majorY / 2.0) > originY)) {
continue;
}
int position = (int) yPositionToPixel(y);
g2.drawString(format(y), (int) axisV + 5, position);
}
}
// Draws the horizontal lines of the grid. Draws both minor and major
// grid lines.
private void drawHorizontalGrid(Graphics2D g2) {
double minorSpacing = majorY / getMinorCountY();
double axisV = xPositionToPixel(originX);
Stroke gridStroke = new BasicStroke(STROKE_GRID);
Stroke axisStroke = new BasicStroke(STROKE_AXIS);
Rectangle clip = g2.getClipBounds();
int position;
if (!isAxisPainted()) {
position = (int) xPositionToPixel(originX);
if (position >= clip.x && position <= clip.x + clip.width) {
g2.setColor(getMajorGridColor());
g2.drawLine(position, clip.y, position, clip.y + clip.height);
}
}
// double startY = Math.floor((minY - originY) / majorY) * majorY;
double startY = Math.floor(minY / majorY) * majorY;
for (double y = startY; y < maxY + majorY; y += majorY) {
g2.setStroke(gridStroke);
g2.setColor(getMinorGridColor());
for (int i = 0; i < getMinorCountY(); i++) {
position = (int) yPositionToPixel(y - i * minorSpacing);
if (position >= clip.y && position <= clip.y + clip.height) {
g2.drawLine(clip.x, position, clip.x + clip.width, position);
}
}
position = (int) yPositionToPixel(y);
if (position >= clip.y && position <= clip.y + clip.height) {
g2.setColor(getMajorGridColor());
g2.drawLine(clip.x, position, clip.x + clip.width, position);
if (isAxisPainted()) {
g2.setStroke(axisStroke);
g2.setColor(getAxisColor());
g2.drawLine((int) axisV - 3, position, (int) axisV + 3, position);
}
}
}
}
// Draws labels on the horizontal axis. First draws labels on the right of
// the origin, then on the left.
private void drawHorizontalAxisLabels(Graphics2D g2) {
double axisH = yPositionToPixel(originY);
FontMetrics metrics = g2.getFontMetrics();
double startX = Math.floor(minX / majorX) * majorX;
for (double x = startX; x < maxX + majorX; x += majorX) {
if (((x - majorX / 2.0) < originX) &&
((x + majorX / 2.0) > originX)) {
continue;
}
int position = (int) xPositionToPixel(x);
g2.drawString(format(x), position,
(int) axisH + metrics.getHeight());
}
}
// Draws the vertical lines of the grid. Draws both minor and major grid lines.
private void drawVerticalGrid(Graphics2D g2) {
double minorSpacing = majorX / getMinorCountX();
double axisH = yPositionToPixel(originY);
Stroke gridStroke = new BasicStroke(STROKE_GRID);
Stroke axisStroke = new BasicStroke(STROKE_AXIS);
Rectangle clip = g2.getClipBounds();
int position;
if (!isAxisPainted()) {
position = (int) yPositionToPixel(originY);
if (position >= clip.y && position <= clip.y + clip.height) {
g2.setColor(getMajorGridColor());
g2.drawLine(clip.x, position, clip.x + clip.width, position);
}
}
double startX = Math.floor(minX / majorX) * majorX;
for (double x = startX; x < maxX + majorX; x += majorX) {
g2.setStroke(gridStroke);
g2.setColor(getMinorGridColor());
for (int i = 0; i < getMinorCountX(); i++) {
position = (int) xPositionToPixel(x - i * minorSpacing);
if (position >= clip.x && position <= clip.x + clip.width) {
g2.drawLine(position, clip.y, position, clip.y + clip.height);
}
}
position = (int) xPositionToPixel(x);
if (position >= clip.x && position <= clip.x + clip.width) {
g2.setColor(getMajorGridColor());
g2.drawLine(position, clip.y, position, clip.y + clip.height);
if (isAxisPainted()) {
g2.setStroke(axisStroke);
g2.setColor(getAxisColor());
g2.drawLine(position, (int) axisH - 3, position, (int) axisH + 3);
}
}
}
}
// Draws the main axis.
private void drawAxis(Graphics2D g2) {
if (!isAxisPainted()) {
return;
}
double axisH = yPositionToPixel(originY);
double axisV = xPositionToPixel(originX);
Rectangle clip = g2.getClipBounds();
g2.setColor(getAxisColor());
Stroke stroke = g2.getStroke();
g2.setStroke(new BasicStroke(STROKE_AXIS));
if (axisH >= clip.y && axisH <= clip.y + clip.height) {
g2.drawLine(clip.x, (int) axisH, clip.x + clip.width, (int) axisH);
}
if (axisV >= clip.x && axisV <= clip.x + clip.width) {
g2.drawLine((int) axisV, clip.y, (int) axisV, clip.y + clip.height);
}
g2.setStroke(stroke);
}
/**
* This method is called by the component prior to any drawing operation
* to configure the drawing surface. The default implementation enables
* antialiasing on the graphics.
* This method can be overriden by subclasses to modify the drawing
* surface before any painting happens.
*
* @param g2 the graphics surface to set up
* @see #paintExtra(Graphics2D)
* @see #paintBackground(Graphics2D)
*/
protected void setupGraphics(Graphics2D g2) {
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
}
/**
* This method is called by the component whenever it needs to paint
* its background. The default implementation fills the background with
* a solid color as defined by {@link #getBackground()}. Background painting
* does not happen when {@link #isBackgroundPainted()} returns false.
* It is recommended to subclasses to honor the contract defined by
* {@link #isBackgroundPainted()} and {@link #setBackgroundPainted(boolean)}.
*
* @param g2 the graphics surface on which the background must be drawn
* @see #setupGraphics(Graphics2D)
* @see #paintExtra(Graphics2D)
* @see #isBackgroundPainted()
* @see #setBackgroundPainted(boolean)
*/
protected void paintBackground(Graphics2D g2) {
if (isBackgroundPainted()) {
Painter super JComponent> p = getBackgroundPainter();
if (p != null) {
p.paint(g2, this, getWidth(), getHeight());
} else {
g2.setColor(getBackground());
g2.fill(g2.getClipBounds());
}
}
}
// Format a number with the appropriate number formatter. Numbers >= 0.01
// and < 100 are formatted with a regular, 2-digits, numbers formatter.
// Other numbers use a scientific notation given by a DecimalFormat instance
private String format(double number) {
boolean farAway = (number != 0.0d && Math.abs(number) < 0.01d) || Math.abs(number) > 99.0d;
return (farAway ? secondFormatter : mainFormatter).format(number);
}
/**
*
A plot represents a mathematical transformation used by
* {@link JXGraph}. When a plot belongs to a graph, the graph component
* asks for the transformation of a value along the X axis. The resulting
* value defines the Y coordinates at which the graph must draw a spot of
* color.
*
* Here is a sample implementation of this class that draws a straight line
* once added to a graph (it follows the well-known equation y=a.x+b):
*
*
* class LinePlot extends JXGraph.Plot {
* public double compute(double value) {
* return 2.0 * value + 1.0;
* }
* }
*
*
* When a plot is added to an instance of
* JXGraph
, the JXGraph
automatically becomes
* a new property change listener of the plot. If property change events are
* fired, the graph will be updated accordingly.
*
* More information about plots usage can be found in {@link JXGraph} in
* the section entitled Plots.
*
* @see JXGraph
* @see JXGraph#addPlots(Color, JXGraph.Plot...)
*/
public abstract static class Plot extends AbstractBean {
/**
* Creates a new, parameter-less plot.
*/
protected Plot() {
}
/**
* This method must return the result of a mathematical
* transformation of its sole parameter.
*
* @param value a value along the X axis of the graph currently
* drawing this plot
* @return the result of the mathematical transformation of value
*/
public abstract double compute(double value);
}
// Encapsulates a plot and its color. Avoids the use of a full-blown Map.
private static class DrawablePlot {
private final Plot equation;
private final Color color;
private DrawablePlot(Plot equation, Color color) {
this.equation = equation;
this.color = color;
}
private Plot getEquation() {
return equation;
}
private Color getColor() {
return color;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
final DrawablePlot that = (DrawablePlot) o;
if (!color.equals(that.color)) {
return false;
}
return equation.equals(that.equation);
}
@Override
public int hashCode() {
int result;
result = equation.hashCode();
result = 29 * result + color.hashCode();
return result;
}
}
// Shrinks or expand the view depending on the mouse wheel direction.
// When the wheel moves down, the view is expanded. Otherwise it is shrunk.
private class ZoomHandler implements MouseWheelListener {
@Override
public void mouseWheelMoved(MouseWheelEvent e) {
double distanceX = maxX - minX;
double distanceY = maxY - minY;
double cursorX = minX + distanceX / 2.0;
double cursorY = minY + distanceY / 2.0;
int rotation = e.getWheelRotation();
if (rotation < 0) {
distanceX /= ZOOM_MULTIPLIER;
distanceY /= ZOOM_MULTIPLIER;
majorX /= ZOOM_MULTIPLIER;
majorY /= ZOOM_MULTIPLIER;
} else {
distanceX *= ZOOM_MULTIPLIER;
distanceY *= ZOOM_MULTIPLIER;
majorX *= ZOOM_MULTIPLIER;
majorY *= ZOOM_MULTIPLIER;
}
minX = cursorX - distanceX / 2.0;
maxX = cursorX + distanceX / 2.0;
minY = cursorY - distanceY / 2.0;
maxY = cursorY + distanceY / 2.0;
repaint();
}
}
// Listens for a click on the middle button of the mouse and resets the view
private class ResetHandler extends MouseAdapter {
@Override
public void mousePressed(MouseEvent e) {
if (e.getButton() != MouseEvent.BUTTON2) {
return;
}
resetView();
}
}
// Starts and ends drag gestures with mouse left button.
private class PanHandler extends MouseAdapter {
@Override
public void mousePressed(MouseEvent e) {
if (e.getButton() != MouseEvent.BUTTON1) {
return;
}
dragStart = e.getPoint();
setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
}
@Override
public void mouseReleased(MouseEvent e) {
if (e.getButton() != MouseEvent.BUTTON1) {
return;
}
setCursor(Cursor.getDefaultCursor());
}
}
// Handles drag gesture with the left mouse button and relocates the view
// accordingly.
private class PanMotionHandler extends MouseMotionAdapter {
@Override
public void mouseDragged(MouseEvent e) {
Point dragEnd = e.getPoint();
double distance = xPixelToPosition(dragEnd.getX()) -
xPixelToPosition(dragStart.getX());
minX = minX - distance;
maxX = maxX - distance;
distance = yPixelToPosition(dragEnd.getY()) -
yPixelToPosition(dragStart.getY());
minY = minY - distance;
maxY = maxY - distance;
repaint();
dragStart = dragEnd;
}
}
}