// **********************************************************************
//
// <copyright>
//
// BBN Technologies
// 10 Moulton Street
// Cambridge, MA 02138
// (617) 873-8000
//
// Copyright (C) BBNT Solutions LLC. All rights reserved.
//
// </copyright>
// **********************************************************************
//
// $Source: /cvs/distapps/openmap/src/openmap/com/bbn/openmap/omGraphics/util/ArcCalc.java,v $
// $RCSfile: ArcCalc.java,v $
// $Revision: 1.5 $
// $Date: 2005/08/10 22:28:13 $
// $Author: dietrick $
//
// **********************************************************************
package com.bbn.openmap.omGraphics.util;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.geom.Point2D;
import java.io.Serializable;
import com.bbn.openmap.MoreMath;
import com.bbn.openmap.omGraphics.OMColor;
import com.bbn.openmap.omGraphics.OMGraphicList;
import com.bbn.openmap.omGraphics.OMLine;
import com.bbn.openmap.omGraphics.OMRect;
import com.bbn.openmap.proj.Projection;
import com.bbn.openmap.util.Debug;
/**
* A class that calculates an arc between two points, given the point
* coordinates, and an arc measurement that represents, in radians,
* the length of the part of the circle that should be represented by
* the arc.
*/
public class ArcCalc implements Serializable {
/** Debugging list showing algorithm points. */
protected transient OMGraphicList arcGraphics = null;
protected transient float[] xpoints;
protected transient float[] ypoints;
/**
* This setting is the amount of an angle, limited to a
* semi-circle (PI) that the curve will represent. In other words,
* the arc between the two end points is going to look like a 0
* degrees of a circle (straight line, which is the default), or
* 180 degrees of a circle (full semi-circle). Given in radians,
* though, not degrees. OK?
*/
protected double arcAngle = 0;
/**
* For x-y and offset lines that have an arc drawn between them,
* tell which way the arc should be drawn, toward the Equator, or
* away from it, generally. Default is true, to make it look like
* great circle line for northern hemisphere lines.
*/
protected boolean arcUp = true;
/**
* Set to true if the points for the arc line up from x2, y2 to
* x1, y1
*/
protected boolean reversed = false;
/**
* Set the arc that is drawn between the points of a x-y or offset
* line. If the arc amount is negative, the arc will be flipped
* over.
*
* @param aa arcAngle, in radians, between 0-PI.
* @param putArcUp arc peak above points.
*/
public ArcCalc(double aa, boolean putArcUp) {
arcAngle = aa;
arcUp = putArcUp;
// If it's negative, flip it over...
if (aa < 0) {
arcAngle *= -1.0;
arcUp = !arcUp;
}
if (arcAngle > Math.PI) {
arcAngle = Math.PI;
}
}
/**
* Return the arc angle set for this line. Will only be set if it
* was set externally.
*
* @return arc angle in radians.
*/
public double getArcAngle() {
return arcAngle;
}
/**
* Returns true if the arc direction setting is upward, meaning
* that the peak of the arc is above (or more so) the line that
* goes between the two points.
*/
public boolean isArcUp() {
return arcUp;
}
/**
* Generate the points that will generate the curved line between
* two points. The arcAngle is the number of radians of a circle
* that the arc should represent. Math.PI is the Max. The
* setArcAngle should be called before this method is called, so
* that the method knows what to create.
*/
public void generate(int x1, int y1, int x2, int y2) {
// The algorithm.
//
// Draw a straight line between the points, and figure out the
// center point between them on the line. Then, on another
// line that is perpendicular to the first line, figure out
// where the point is that will act as a center of a circle.
// That circle needs to pass through both points, and the
// radius is such that the arc angle of the circle between the
// points is the same as the arcAngle set for the ArcCalc.
// Then, the arc needs to be generated. This is done by
// looking at the circle, and figuring out the angle (from 0
// to 2PI) that the line from the center to point 1, and then
// the center to point 2. This gives us the angular extents
// of the arc. Then we need to figure out the angle
// increments needed to get good coordinates for the arc.
// Then, starting at the low arc angle, we increment it to get
// the coordinates for the arced line, a given radius away
// from the circle center, between the arc angle extents.
Point midPoint = new Point();
Point arcCenter = new Point();
Point2D peakPoint = new Point2D.Float();
// pixel distance between points.
double distance = Math.sqrt(Math.pow(Math.abs(y2 - y1), 2.0)
+ Math.pow(Math.abs(x2 - x1), 2.0));
// slope of straight line between points.
double straightLineSlope = Math.atan((double) (y2 - y1)
/ (double) (x2 - x1));
// slope of line that the arc focus will reside on.
double inverseSlope = straightLineSlope - (Math.PI / 2.0);
if (Debug.debugging("arc")) {
Debug.output("ArcCalc.generate: Slope is "
+ Math.toDegrees(straightLineSlope)
+ " degrees, distance = " + distance + " pixels.");
}
// centerX/Y is the midpoint between the two points.
midPoint.setLocation(x1 + ((x2 - x1) / 2), y1 + ((y2 - y1) / 2));
if (Debug.debugging("arc")) {
Debug.output("ArcCalc.generate: Center point for (" + x1 + ", "
+ y1 + ") to (" + x2 + ", " + y2 + ") is (" + midPoint.x
+ ", " + midPoint.y + ")");
}
double arccos = Math.cos(arcAngle);
double arcRadius;
if (arccos != 1.0) {
arcRadius = distance / Math.sqrt(2.0 * (1.0 - Math.cos(arcAngle)));
} else {
arcRadius = distance / Math.sqrt(2.0);
}
if (Debug.debugging("arc")) {
Debug.output("ArcCalc.generate: radius of arc = " + arcRadius);
}
// R' is the distance down the inverse negative slope of the
// line that the focus of the arc is located.
// x is the distance along the right leg of the arc that is
// left over after Rcos(arcAngle) is subtracted from it, in
// order to derive the angle of the straight line between the
// two points.
double x = arcRadius - arcRadius * Math.cos(arcAngle);
double rPrime = (distance / 2.0)
* (Math.sqrt(1.0 - Math.pow(x / distance, 2.0)))
/ Math.sin(arcAngle / 2.0);
if (Debug.debugging("arc")) {
Debug.output("ArcCalc.generate: rPrime = " + rPrime);
}
int direction = 1;
if (arcUp)
direction = -1;
// arcCenter.x and arcCenter.y are the coordinates of the
// focus of the Arc.
arcCenter.x = midPoint.x
+ (direction * (int) (rPrime * Math.cos(inverseSlope)));
arcCenter.y = midPoint.y
+ (direction * (int) (rPrime * Math.sin(inverseSlope)));
if (Debug.debugging("arc")) {
Debug.output("ArcCalc.generateArc: creating supplimental graphics list");
arcGraphics = new OMGraphicList();
double dist1 = Math.sqrt(Math.pow((double) (arcCenter.x - x1), 2.0)
+ Math.pow((double) (arcCenter.y - y1), 2.0));
double dist2 = Math.sqrt(Math.pow((double) (arcCenter.x - x2), 2.0)
+ Math.pow((double) (arcCenter.y - y2), 2.0));
Debug.output("ArcCalc.generate: Center focus for arc is ("
+ arcCenter.x + ", " + arcCenter.y
+ ") along slope line of "
+ Math.toDegrees(inverseSlope) + " degrees).");
Debug.output("ArcCalc.generate: Distance to point 1 from arc focus = "
+ dist1
+ "\n Distance to point 2 from arc focus = "
+ dist2);
// Let's highlight the end points.
OMRect point1 = new OMRect(x1 - 1, y1 - 1, x1 + 1, y1 + 1);
OMRect point2 = new OMRect(x2 - 1, y2 - 1, x2 + 1, y2 + 1);
OMRect arcPoint = new OMRect(arcCenter.x - 1, arcCenter.y - 1, arcCenter.x + 1, arcCenter.y + 1);
point1.setLinePaint(OMColor.red);
point2.setLinePaint(OMColor.red);
arcPoint.setLinePaint(OMColor.blue);
arcGraphics.add(point1);
arcGraphics.add(point2);
arcGraphics.add(arcPoint);
OMLine line1 = new OMLine(x1, y1, x2, y2);
OMLine line2 = new OMLine(midPoint.x, midPoint.y, arcCenter.x, arcCenter.y);
arcGraphics.add(line1);
arcGraphics.add(line2);
}
int realCount = 0;
// Figure out the arc extents for each endpoint. I think
// it's easier to keep track of the angles if they are always
// positive, and we always go from smaller to larger.
double startSlope = getRealAngle((float)arcCenter.getX(), (float)arcCenter.getY(), x1, y1);
double endSlope = getRealAngle((float)arcCenter.getX(), (float)arcCenter.getY(), x2, y2);
double smallSlope, largeSlope;
double angleIncrement;
smallSlope = (startSlope > endSlope) ? endSlope : startSlope;
largeSlope = (smallSlope == startSlope) ? endSlope : startSlope;
// Have to make sure we take the smaller arc around the
// circle.
while (Math.abs(smallSlope - largeSlope) > Math.PI) {
if (Math.abs(largeSlope - smallSlope - Math.PI) < .001) {
// Catch 180 degree angles that are close enough...
break;
}
Debug.message("arc",
"ArcCalc.generate: Modifying the starting slope.");
double tmpSlope = smallSlope + MoreMath.TWO_PI;
smallSlope = largeSlope;
largeSlope = tmpSlope;
}
// Experienced some trouble with vertical and horizontal half
// circles. This took care of that.
if (MoreMath.approximately_equal(arcAngle, Math.PI) && arcUp) {
Debug.message("arc",
"ArcCalc.generate: Modifying 180 angle points.");
double tmpSlope = smallSlope + MoreMath.TWO_PI;
smallSlope = largeSlope;
largeSlope = tmpSlope;
}
// Figure out the angle increment for grabbing coordinates -
// use the larger dimension of the arc end point differences.
if (Math.abs(y2 - y1) < Math.abs(x2 - x1)) {
angleIncrement = Math.PI / Math.abs(x2 - x1);
} else {
angleIncrement = Math.PI / Math.abs(y2 - y1);
}
int numPoints = (int) (Math.abs(smallSlope - largeSlope)
/ angleIncrement + 2);
float[] xPoints = new float[numPoints];
float[] yPoints = new float[numPoints];
if (Debug.debugging("arc")) {
Debug.output("ArcCalc.generate: angle to x1, y1 is " + startSlope
+ " (" + Math.toDegrees(startSlope)
+ " degrees), angle to x2, y2 is " + endSlope + " ("
+ Math.toDegrees(endSlope) + " degrees)");
Debug.output("ArcCalc.generate: Starting angle is " + smallSlope
+ "(" + Math.toDegrees(smallSlope)
+ " degrees), end angle is " + largeSlope + " ("
+ Math.toDegrees(largeSlope)
+ " degrees), incrementing by " + angleIncrement + " ("
+ Math.toDegrees(angleIncrement) + " degrees)");
}
reversed = false;
// Get the coordinates of the arc from the arc extents.
while (smallSlope < largeSlope && realCount < numPoints) {
xPoints[realCount] = arcCenter.x
+ (int) (arcRadius * Math.cos(smallSlope));
yPoints[realCount] = arcCenter.y
+ (int) (arcRadius * Math.sin(smallSlope));
if (realCount == 0 && xPoints[realCount] == x2) {
Debug.message("arc", "ArcCalc: line reversed");
reversed = true;
}
if (Debug.debugging("arc") && realCount == 0) {
OMLine startLine = new OMLine(arcCenter.x, arcCenter.y, (int) xPoints[0], (int) yPoints[0]);
startLine.setLinePaint(OMColor.white);
arcGraphics.add(startLine);
} else if (Debug.debugging("arcdetail")) {
Debug.output(" angle " + smallSlope + " (" + smallSlope * 180
/ Math.PI + " degrees) = " + xPoints[realCount] + ", "
+ yPoints[realCount]);
}
if (Math.abs(largeSlope - smallSlope - (arcAngle / 2.0)) < angleIncrement) {
// Found the halfway point, mark it...
peakPoint.setLocation(xPoints[realCount], yPoints[realCount]);
Debug.message("arc", "ArcCalc: Found a midpoint.");
}
smallSlope += angleIncrement;
realCount++;
}
// Give the coordinates to the OMLine.
xpoints = new float[realCount];
ypoints = new float[realCount];
System.arraycopy(xPoints, 0, xpoints, 0, realCount);
System.arraycopy(yPoints, 0, ypoints, 0, realCount);
}
/**
* Given the straight line between two points, figure out the
* angle, in radians, of that line in relation to the coordinate
* system on the screen. Always returns a positive value, and the
* angle is from point 1 to point 2.
*/
protected double getRealAngle(float x1, float y1, float x2, float y2) {
double angle = 0;
double horDiff = (double) (x2 - x1);
double vertDiff = (double) (y2 - y1);
// If there is no horizontal difference, then it's pointing
// up or down.
if (horDiff == 0) {
if (vertDiff > 0) {
angle = MoreMath.HALF_PI;
} else if (vertDiff < 0) {
angle = -MoreMath.HALF_PI;
}
} else {
angle = Math.atan(vertDiff / horDiff);
// It's pointed in the wrong direction... fix it here.
if (horDiff < 0) {
angle += Math.PI;
}
}
// Either way, I think we want to make the angle positive.
while (angle < 0) {
angle += MoreMath.TWO_PI;
}
return angle;
}
public float[] getXPoints() {
return xpoints;
}
public float[] getYPoints() {
return ypoints;
}
public void generate(Projection proj) {
if (proj != null && arcGraphics != null) {
arcGraphics.generate(proj);
}
}
public void render(Graphics g) {
if (arcGraphics != null) {
Debug.output("OMLine rendering " + arcGraphics.size()
+ " arcGraphics.");
arcGraphics.render(g);
}
}
public OMGraphicList getArcGraphics() {
if (arcGraphics == null) {
return new OMGraphicList();
} else {
return arcGraphics;
}
}
public boolean getReversed() {
return reversed;
}
}