/*
* 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.geom.GeneralPath;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import org.opensourcephysics.numerics.ArrayLib;
/**
* Library of useful computer graphics routines such as geometry routines
* for computing the intersection of different shapes and rendering methods
* for computing bounds and performing optimized drawing.
*
* @author <a href="http://jheer.org">jeffrey heer</a>
*/
public class GraphicsLib {
/** Indicates no intersection between shapes */
public static final int NO_INTERSECTION = 0;
/** Indicates intersection between shapes */
public static final int COINCIDENT = -1;
/** Indicates two lines are parallel */
public static final int PARALLEL = -2;
/**
* Compute the intersection of two line segments.
* @param a the first line segment
* @param b the second line segment
* @param intersect a Point in which to store the intersection point
* @return the intersection code. One of {@link #NO_INTERSECTION},
* {@link #COINCIDENT}, or {@link #PARALLEL}.
*/
public static int intersectLineLine(Line2D a, Line2D b, Point2D intersect) {
double a1x = a.getX1(), a1y = a.getY1();
double a2x = a.getX2(), a2y = a.getY2();
double b1x = b.getX1(), b1y = b.getY1();
double b2x = b.getX2(), b2y = b.getY2();
return intersectLineLine(a1x, a1y, a2x, a2y, b1x, b1y, b2x, b2y, intersect);
}
/**
* Compute the intersection of two line segments.
* @param a1x the x-coordinate of the first endpoint of the first line
* @param a1y the y-coordinate of the first endpoint of the first line
* @param a2x the x-coordinate of the second endpoint of the first line
* @param a2y the y-coordinate of the second endpoint of the first line
* @param b1x the x-coordinate of the first endpoint of the second line
* @param b1y the y-coordinate of the first endpoint of the second line
* @param b2x the x-coordinate of the second endpoint of the second line
* @param b2y the y-coordinate of the second endpoint of the second line
* @param intersect a Point in which to store the intersection point
* @return the intersection code. One of {@link #NO_INTERSECTION},
* {@link #COINCIDENT}, or {@link #PARALLEL}.
*/
public static int intersectLineLine(double a1x, double a1y, double a2x, double a2y, double b1x, double b1y, double b2x, double b2y, Point2D intersect) {
double ua_t = (b2x-b1x)*(a1y-b1y)-(b2y-b1y)*(a1x-b1x);
double ub_t = (a2x-a1x)*(a1y-b1y)-(a2y-a1y)*(a1x-b1x);
double u_b = (b2y-b1y)*(a2x-a1x)-(b2x-b1x)*(a2y-a1y);
if(u_b!=0) {
double ua = ua_t/u_b;
double ub = ub_t/u_b;
if((0<=ua)&&(ua<=1)&&(0<=ub)&&(ub<=1)) {
intersect.setLocation(a1x+ua*(a2x-a1x), a1y+ua*(a2y-a1y));
return 1;
}
return NO_INTERSECTION;
}
return(((ua_t==0)||(ub_t==0)) ? COINCIDENT : PARALLEL);
}
/**
* Compute the intersection of a line and a rectangle.
* @param a1 the first endpoint of the line
* @param a2 the second endpoint of the line
* @param r the rectangle
* @param pts a length 2 or greater array of points in which to store
* the results
* @return the intersection code. One of {@link #NO_INTERSECTION},
* {@link #COINCIDENT}, or {@link #PARALLEL}.
*/
public static int intersectLineRectangle(Point2D a1, Point2D a2, Rectangle2D r, Point2D[] pts) {
double a1x = a1.getX(), a1y = a1.getY();
double a2x = a2.getX(), a2y = a2.getY();
double mxx = r.getMaxX(), mxy = r.getMaxY();
double mnx = r.getMinX(), mny = r.getMinY();
if(pts[0]==null) {
pts[0] = new Point2D.Double();
}
if(pts[1]==null) {
pts[1] = new Point2D.Double();
}
int i = 0;
if(intersectLineLine(mnx, mny, mxx, mny, a1x, a1y, a2x, a2y, pts[i])>0) {
i++;
}
if(intersectLineLine(mxx, mny, mxx, mxy, a1x, a1y, a2x, a2y, pts[i])>0) {
i++;
}
if(i==2) {
return i;
}
if(intersectLineLine(mxx, mxy, mnx, mxy, a1x, a1y, a2x, a2y, pts[i])>0) {
i++;
}
if(i==2) {
return i;
}
if(intersectLineLine(mnx, mxy, mnx, mny, a1x, a1y, a2x, a2y, pts[i])>0) {
i++;
}
return i;
}
/**
* Compute the intersection of a line and a rectangle.
* @param l the line
* @param r the rectangle
* @param pts a length 2 or greater array of points in which to store
* the results
* @return the intersection code. One of {@link #NO_INTERSECTION},
* {@link #COINCIDENT}, or {@link #PARALLEL}.
*/
public static int intersectLineRectangle(Line2D l, Rectangle2D r, Point2D[] pts) {
double a1x = l.getX1(), a1y = l.getY1();
double a2x = l.getX2(), a2y = l.getY2();
double mxx = r.getMaxX(), mxy = r.getMaxY();
double mnx = r.getMinX(), mny = r.getMinY();
if(pts[0]==null) {
pts[0] = new Point2D.Double();
}
if(pts[1]==null) {
pts[1] = new Point2D.Double();
}
int i = 0;
if(intersectLineLine(mnx, mny, mxx, mny, a1x, a1y, a2x, a2y, pts[i])>0) {
i++;
}
if(intersectLineLine(mxx, mny, mxx, mxy, a1x, a1y, a2x, a2y, pts[i])>0) {
i++;
}
if(i==2) {
return i;
}
if(intersectLineLine(mxx, mxy, mnx, mxy, a1x, a1y, a2x, a2y, pts[i])>0) {
i++;
}
if(i==2) {
return i;
}
if(intersectLineLine(mnx, mxy, mnx, mny, a1x, a1y, a2x, a2y, pts[i])>0) {
i++;
}
return i;
}
/**
* Computes the 2D convex hull of a set of points using Graham's
* scanning algorithm. The algorithm has been implemented as described
* in Cormen, Leiserson, and Rivest's Introduction to Algorithms.
*
* The running time of this algorithm is O(n log n), where n is
* the number of input points.
*
* @param pts the input points in [x0,y0,x1,y1,...] order
* @param len the length of the pts array to consider (2 * #points)
* @return the convex hull of the input points
*/
public static double[] convexHull(double[] pts, int len) {
if(len<6) {
throw new IllegalArgumentException("Input must have at least 3 points"); //$NON-NLS-1$
}
int plen = len/2-1;
float[] angles = new float[plen];
int[] idx = new int[plen];
int[] stack = new int[len/2];
return convexHull(pts, len, angles, idx, stack);
}
/**
* Computes the 2D convex hull of a set of points using Graham's
* scanning algorithm. The algorithm has been implemented as described
* in Cormen, Leiserson, and Rivest's Introduction to Algorithms.
*
* The running time of this algorithm is O(n log n), where n is
* the number of input points.
*
* @param pts
* @return the convex hull of the input points
*/
public static double[] convexHull(double[] pts, int len, float[] angles, int[] idx, int[] stack) {
// check arguments
int plen = len/2-1;
if(len<6) {
throw new IllegalArgumentException("Input must have at least 3 points"); //$NON-NLS-1$
}
if((angles.length<plen)||(idx.length<plen)||(stack.length<len/2)) {
throw new IllegalArgumentException("Pre-allocated data structure too small"); //$NON-NLS-1$
}
int i0 = 0;
// find the starting ref point: leftmost point with the minimum y coord
for(int i = 2; i<len; i += 2) {
if(pts[i+1]<pts[i0+1]) {
i0 = i;
} else if(pts[i+1]==pts[i0+1]) {
i0 = ((pts[i]<pts[i0]) ? i : i0);
}
}
// calculate polar angles from ref point and sort
for(int i = 0, j = 0; i<len; i += 2) {
if(i==i0) {
continue;
}
angles[j] = (float) Math.atan2(pts[i+1]-pts[i0+1], pts[i]-pts[i0]);
idx[j++] = i;
}
ArrayLib.sort(angles, idx, plen);
// toss out duplicated angles
float angle = angles[0];
int ti = 0, tj = idx[0];
for(int i = 1; i<plen; i++) {
int j = idx[i];
if(angle==angles[i]) {
// keep whichever angle corresponds to the most distant
// point from the reference point
double x1 = pts[tj]-pts[i0];
double y1 = pts[tj+1]-pts[i0+1];
double x2 = pts[j]-pts[i0];
double y2 = pts[j+1]-pts[i0+1];
double d1 = x1*x1+y1*y1;
double d2 = x2*x2+y2*y2;
if(d1>=d2) {
idx[i] = -1;
} else {
idx[ti] = -1;
angle = angles[i];
ti = i;
tj = j;
}
} else {
angle = angles[i];
ti = i;
tj = j;
}
}
// initialize our stack
int sp = 0;
stack[sp++] = i0;
int j = 0;
for(int k = 0; k<2; j++) {
if(idx[j]!=-1) {
stack[sp++] = idx[j];
k++;
}
}
// do graham's scan
for(; j<plen; j++) {
if(idx[j]==-1) {
continue; // skip tossed out points
}
while(isNonLeft(i0, stack[sp-2], stack[sp-1], idx[j], pts)) {
sp--;
}
stack[sp++] = idx[j];
}
// construct the hull
double[] hull = new double[2*sp];
for(int i = 0; i<sp; i++) {
hull[2*i] = pts[stack[i]];
hull[2*i+1] = pts[stack[i]+1];
}
return hull;
}
/**
* Convex hull helper method for detecting a non left turn about 3 points
*/
private static boolean isNonLeft(int i0, int i1, int i2, int i3, double[] pts) {
double l1, l2, l4, l5, l6, angle1, angle2, angle;
l1 = Math.sqrt(Math.pow(pts[i2+1]-pts[i1+1], 2)+Math.pow(pts[i2]-pts[i1], 2));
l2 = Math.sqrt(Math.pow(pts[i3+1]-pts[i2+1], 2)+Math.pow(pts[i3]-pts[i2], 2));
l4 = Math.sqrt(Math.pow(pts[i3+1]-pts[i0+1], 2)+Math.pow(pts[i3]-pts[i0], 2));
l5 = Math.sqrt(Math.pow(pts[i1+1]-pts[i0+1], 2)+Math.pow(pts[i1]-pts[i0], 2));
l6 = Math.sqrt(Math.pow(pts[i2+1]-pts[i0+1], 2)+Math.pow(pts[i2]-pts[i0], 2));
angle1 = Math.acos(((l2*l2)+(l6*l6)-(l4*l4))/(2*l2*l6));
angle2 = Math.acos(((l6*l6)+(l1*l1)-(l5*l5))/(2*l6*l1));
angle = (Math.PI-angle1)-angle2;
if(angle<=0.0) {
return(true);
}
return(false);
}
/**
* Computes the mean, or centroid, of a set of points
* @param pts the points array, in x1, y1, x2, y2, ... arrangement.
* @param len the length of the array to consider
* @return the centroid as a length-2 float array
*/
public static float[] centroid(float pts[], int len) {
float[] c = new float[] {0, 0};
for(int i = 0; i<len; i += 2) {
c[0] += pts[i];
c[1] += pts[i+1];
}
c[0] /= len/2;
c[1] /= len/2;
return c;
}
/**
* Expand a polygon by adding the given distance along the line from
* the centroid of the polyong.
* @param pts the polygon to expand, a set of points in a float array
* @param len the length of the range of the array to consider
* @param amt the amount by which to expand the polygon, each point
* will be moved this distance along the line from the centroid of the
* polygon to the given point.
*/
public static void growPolygon(float pts[], int len, float amt) {
float[] c = centroid(pts, len);
for(int i = 0; i<len; i += 2) {
float vx = pts[i]-c[0];
float vy = pts[i+1]-c[1];
float norm = (float) Math.sqrt(vx*vx+vy*vy);
pts[i] += amt*vx/norm;
pts[i+1] += amt*vy/norm;
}
}
/**
* Compute a cardinal spline, a series of cubic Bezier splines smoothly
* connecting a set of points. Cardinal splines maintain C(1)
* continuity, ensuring the connected spline segments form a differentiable
* curve, ensuring at least a minimum level of smoothness.
* @param pts the points to interpolate with a cardinal spline
* @param slack a parameter controlling the "tightness" of the spline to
* the control points, 0.10 is a typically suitable value
* @param closed true if the cardinal spline should be closed (i.e. return
* to the starting point), false for an open curve
* @return the cardinal spline as a Java2D {@link java.awt.geom.GeneralPath}
* instance.
*/
public static GeneralPath cardinalSpline(float pts[], float slack, boolean closed) {
GeneralPath path = new GeneralPath();
path.moveTo(pts[0], pts[1]);
return cardinalSpline(path, pts, slack, closed, 0f, 0f);
}
/**
* Compute a cardinal spline, a series of cubic Bezier splines smoothly
* connecting a set of points. Cardinal splines maintain C(1)
* continuity, ensuring the connected spline segments form a differentiable
* curve, ensuring at least a minimum level of smoothness.
* @param pts the points to interpolate with a cardinal spline
* @param start the starting index from which to read points
* @param npoints the number of points to consider
* @param slack a parameter controlling the "tightness" of the spline to
* the control points, 0.10 is a typically suitable value
* @param closed true if the cardinal spline should be closed (i.e. return
* to the starting point), false for an open curve
* @return the cardinal spline as a Java2D {@link java.awt.geom.GeneralPath}
* instance.
*/
public static GeneralPath cardinalSpline(float pts[], int start, int npoints, float slack, boolean closed) {
GeneralPath path = new GeneralPath();
path.moveTo(pts[start], pts[start+1]);
return cardinalSpline(path, pts, start, npoints, slack, closed, 0f, 0f);
}
/**
* Compute a cardinal spline, a series of cubic Bezier splines smoothly
* connecting a set of points. Cardinal splines maintain C(1)
* continuity, ensuring the connected spline segments form a differentiable
* curve, ensuring at least a minimum level of smoothness.
* @param p the GeneralPath instance to use to store the result
* @param pts the points to interpolate with a cardinal spline
* @param slack a parameter controlling the "tightness" of the spline to
* the control points, 0.10 is a typically suitable value
* @param closed true if the cardinal spline should be closed (i.e. return
* to the starting point), false for an open curve
* @param tx a value by which to translate the curve along the x-dimension
* @param ty a value by which to translate the curve along the y-dimension
* @return the cardinal spline as a Java2D {@link java.awt.geom.GeneralPath}
* instance.
*/
public static GeneralPath cardinalSpline(GeneralPath p, float pts[], float slack, boolean closed, float tx, float ty) {
int npoints = 0;
for(; npoints<pts.length; ++npoints) {
if(Float.isNaN(pts[npoints])) {
break;
}
}
return cardinalSpline(p, pts, 0, npoints/2, slack, closed, tx, ty);
}
/**
* Compute a cardinal spline, a series of cubic Bezier splines smoothly
* connecting a set of points. Cardinal splines maintain C(1)
* continuity, ensuring the connected spline segments form a differentiable
* curve, ensuring at least a minimum level of smoothness.
* @param p the GeneralPath instance to use to store the result
* @param pts the points to interpolate with a cardinal spline
* @param start the starting index from which to read points
* @param npoints the number of points to consider
* @param slack a parameter controlling the "tightness" of the spline to
* the control points, 0.10 is a typically suitable value
* @param closed true if the cardinal spline should be closed (i.e. return
* to the starting point), false for an open curve
* @param tx a value by which to translate the curve along the x-dimension
* @param ty a value by which to translate the curve along the y-dimension
* @return the cardinal spline as a Java2D {@link java.awt.geom.GeneralPath}
* instance.
*/
public static GeneralPath cardinalSpline(GeneralPath p, float pts[], int start, int npoints, float slack, boolean closed, float tx, float ty) {
// compute the size of the path
int len = 2*npoints;
int end = start+len;
if(len<6) {
throw new IllegalArgumentException("To create spline requires at least 3 points"); //$NON-NLS-1$
}
float dx1, dy1, dx2, dy2;
// compute first control point
if(closed) {
dx2 = pts[start+2]-pts[end-2];
dy2 = pts[start+3]-pts[end-1];
} else {
dx2 = pts[start+4]-pts[start];
dy2 = pts[start+5]-pts[start+1];
}
// repeatedly compute next control point and append curve
int i;
for(i = start+2; i<end-2; i += 2) {
dx1 = dx2;
dy1 = dy2;
dx2 = pts[i+2]-pts[i-2];
dy2 = pts[i+3]-pts[i-1];
p.curveTo(tx+pts[i-2]+slack*dx1, ty+pts[i-1]+slack*dy1, tx+pts[i]-slack*dx2, ty+pts[i+1]-slack*dy2, tx+pts[i], ty+pts[i+1]);
}
// compute last control point
if(closed) {
dx1 = dx2;
dy1 = dy2;
dx2 = pts[start]-pts[i-2];
dy2 = pts[start+1]-pts[i-1];
p.curveTo(tx+pts[i-2]+slack*dx1, ty+pts[i-1]+slack*dy1, tx+pts[i]-slack*dx2, ty+pts[i+1]-slack*dy2, tx+pts[i], ty+pts[i+1]);
dx1 = dx2;
dy1 = dy2;
dx2 = pts[start+2]-pts[end-2];
dy2 = pts[start+3]-pts[end-1];
p.curveTo(tx+pts[end-2]+slack*dx1, ty+pts[end-1]+slack*dy1, tx+pts[0]-slack*dx2, ty+pts[1]-slack*dy2, tx+pts[0], ty+pts[1]);
p.closePath();
} else {
p.curveTo(tx+pts[i-2]+slack*dx2, ty+pts[i-1]+slack*dy2, tx+pts[i]-slack*dx2, ty+pts[i+1]-slack*dy2, tx+pts[i], ty+pts[i+1]);
}
return p;
}
/**
* Computes a set of curves using the cardinal spline approach, but
* using straight lines for completely horizontal or vertical segments.
* @param p the GeneralPath instance to use to store the result
* @param pts the points to interpolate with the spline
* @param epsilon threshold value under which to treat the difference
* between two values to be zero. Used to determine which segments to
* treat as lines rather than curves.
* @param slack a parameter controlling the "tightness" of the spline to
* the control points, 0.10 is a typically suitable value
* @param closed true if the spline should be closed (i.e. return
* to the starting point), false for an open curve
* @param tx a value by which to translate the curve along the x-dimension
* @param ty a value by which to translate the curve along the y-dimension
* @return the stack spline as a Java2D {@link java.awt.geom.GeneralPath}
* instance.
*/
public static GeneralPath stackSpline(GeneralPath p, float[] pts, float epsilon, float slack, boolean closed, float tx, float ty) {
int npoints = 0;
for(; npoints<pts.length; ++npoints) {
if(Float.isNaN(pts[npoints])) {
break;
}
}
return stackSpline(p, pts, 0, npoints/2, epsilon, slack, closed, tx, ty);
}
/**
* Computes a set of curves using the cardinal spline approach, but
* using straight lines for completely horizontal or vertical segments.
* @param p the GeneralPath instance to use to store the result
* @param pts the points to interpolate with the spline
* @param start the starting index from which to read points
* @param npoints the number of points to consider
* @param epsilon threshold value under which to treat the difference
* between two values to be zero. Used to determine which segments to
* treat as lines rather than curves.
* @param slack a parameter controlling the "tightness" of the spline to
* the control points, 0.10 is a typically suitable value
* @param closed true if the spline should be closed (i.e. return
* to the starting point), false for an open curve
* @param tx a value by which to translate the curve along the x-dimension
* @param ty a value by which to translate the curve along the y-dimension
* @return the stack spline as a Java2D {@link java.awt.geom.GeneralPath}
* instance.
*/
public static GeneralPath stackSpline(GeneralPath p, float pts[], int start, int npoints, float epsilon, float slack, boolean closed, float tx, float ty) {
// compute the size of the path
int len = 2*npoints;
int end = start+len;
if(len<6) {
throw new IllegalArgumentException("To create spline requires at least 3 points"); //$NON-NLS-1$
}
float dx1, dy1, dx2, dy2;
// compute first control point
if(closed) {
dx2 = pts[start+2]-pts[end-2];
dy2 = pts[start+3]-pts[end-1];
} else {
dx2 = pts[start+4]-pts[start];
dy2 = pts[start+5]-pts[start+1];
}
// repeatedly compute next control point and append curve
int i;
for(i = start+2; i<end-2; i += 2) {
dx1 = dx2;
dy1 = dy2;
dx2 = pts[i+2]-pts[i-2];
dy2 = pts[i+3]-pts[i-1];
if((Math.abs(pts[i]-pts[i-2])<epsilon)||(Math.abs(pts[i+1]-pts[i-1])<epsilon)) {
p.lineTo(tx+pts[i], ty+pts[i+1]);
} else {
p.curveTo(tx+pts[i-2]+slack*dx1, ty+pts[i-1]+slack*dy1, tx+pts[i]-slack*dx2, ty+pts[i+1]-slack*dy2, tx+pts[i], ty+pts[i+1]);
}
}
// compute last control point
dx1 = dx2;
dy1 = dy2;
dx2 = pts[start]-pts[i-2];
dy2 = pts[start+1]-pts[i-1];
if((Math.abs(pts[i]-pts[i-2])<epsilon)||(Math.abs(pts[i+1]-pts[i-1])<epsilon)) {
p.lineTo(tx+pts[i], ty+pts[i+1]);
} else {
p.curveTo(tx+pts[i-2]+slack*dx1, ty+pts[i-1]+slack*dy1, tx+pts[i]-slack*dx2, ty+pts[i+1]-slack*dy2, tx+pts[i], ty+pts[i+1]);
}
// close the curve if requested
if(closed) {
if((Math.abs(pts[end-2]-pts[0])<epsilon)||(Math.abs(pts[end-1]-pts[1])<epsilon)) {
p.lineTo(tx+pts[0], ty+pts[1]);
} else {
dx1 = dx2;
dy1 = dy2;
dx2 = pts[start+2]-pts[end-2];
dy2 = pts[start+3]-pts[end-1];
p.curveTo(tx+pts[end-2]+slack*dx1, ty+pts[end-1]+slack*dy1, tx+pts[0]-slack*dx2, ty+pts[1]-slack*dy2, tx+pts[0], ty+pts[1]);
}
p.closePath();
}
return p;
}
/**
* Expand a rectangle by the given amount.
* @param r the rectangle to expand
* @param amount the amount by which to expand the rectangle
*/
public static void expand(Rectangle2D r, double amount) {
r.setRect(r.getX()-amount, r.getY()-amount, r.getWidth()+2*amount, r.getHeight()+2*amount);
}
// ------------------------------------------------------------------------
} // end of class GraphicsLib
/*
* 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
*/