/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2002-2008, Open Source Geospatial Foundation (OSGeo) * * 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; * version 2.1 of the License. * * 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. * * Originally this file is part of Hibernate Spatial, an extension to the * hibernate ORM solution for geographic data. * * Copyright © 2007 Geovise BVBA * Copyright © 2007 K.U. Leuven LRD, Spatial Applications Division, Belgium * * This work was partially supported by the European Commission, * under the 6th Framework Programme, contract IST-2-004688-STP. */ package org.geotools.gml3; import com.vividsolutions.jts.geom.Coordinate; import com.vividsolutions.jts.geom.PrecisionModel; import java.util.ArrayList; import java.util.List; /** * This class provides operations for handling the usage of Circles and arcs in * Geometries. * <p/> * Date: Oct 15, 2007 * * @author Tom Acree * * @source $URL: http://svn.osgeo.org/geotools/trunk/modules/extension/xsd/xsd-gml3/src/main/java/org/geotools/gml3/Circle.java $ */ public class Circle { private Coordinate center = new Coordinate(0.0, 0.0); private double radius = 0; private PrecisionModel precisionModel = new PrecisionModel(); // Constructors ********************************************************** /** * Creates a circle whose center is at the origin and whose radius is 0. */ protected Circle() { } /** * Create a circle with a defined center and radius * * @param center The coordinate representing the center of the circle * @param radius The radius of the circle */ public Circle(Coordinate center, double radius) { this.center = center; this.radius = radius; } /** * Create a circle using the x/y coordinates for the center. * * @param xCenter The x coordinate of the circle's center * @param yCenter The y coordinate of the circle's center * @param radius the radius of the circle */ public Circle(double xCenter, double yCenter, double radius) { this(new Coordinate(xCenter, yCenter), radius); } /** * Creates a circle based on bounding box. It is possible for the user of * this class to pass bounds to this method that do not represent a square. * If this is the case, we must force the bounding rectangle to be a square. * To this end, we check the box and set the side of the box to the larger * dimension of the rectangle * * @param xLeft * @param yUpper * @param xRight * @param yLower */ public Circle(double xLeft, double yUpper, double xRight, double yLower) { double side = Math.min(Math.abs(xRight - xLeft), Math.abs(yLower - yUpper)); this.center.x = Math.min(xRight, xLeft) + side / 2; this.center.y = Math.min(yUpper, yLower) + side / 2; this.radius = side / 2; } /** * Three point method of circle construction. All three points must be on * the circumference of the circle. * * @param point1 * @param point2 * @param point3 */ public Circle(Coordinate point1, Coordinate point2, Coordinate point3) { initThreePointCircle(point1, point2, point3); } /** * Three point method of circle construction. All three points must be on * the circumference of the circle. * * @param x1 * @param y1 * @param x2 * @param y2 * @param x3 * @param y3 */ public Circle(double x1, double y1, double x2, double y2, double x3, double y3) { this(new Coordinate(x1, y1), new Coordinate(x2, y2), new Coordinate(x3, y3)); } /** * shift the center of the circle by delta X and delta Y */ public void shift(double deltaX, double deltaY) { this.center.x = this.center.x + deltaX; this.center.y = this.center.y + deltaY; } /** * Move the circle to a new center */ public void move(double x, double y) { this.center.x = x; this.center.y = y; } /** * Defines the circle based on three points. All three points must be on on * the circumference of the circle, and hence, the 3 points cannot be have * any pair equal, and cannot form a line. Therefore, each point given is * one radius measure from the circle's center. * * @param p1 A point on the desired circle * @param p2 A point on the desired circle * @param p3 A point on the desired circle */ private void initThreePointCircle(Coordinate p1, Coordinate p2, Coordinate p3) { double a13, b13, c13; double a23, b23, c23; double x = 0., y = 0., rad = 0.; // begin pre-calculations for linear system reduction a13 = 2 * (p1.x - p3.x); b13 = 2 * (p1.y - p3.y); c13 = (p1.y * p1.y - p3.y * p3.y) + (p1.x * p1.x - p3.x * p3.x); a23 = 2 * (p2.x - p3.x); b23 = 2 * (p2.y - p3.y); c23 = (p2.y * p2.y - p3.y * p3.y) + (p2.x * p2.x - p3.x * p3.x); // testsuite-suite to be certain we have three distinct points passed double smallNumber = 0.01; if ((Math.abs(a13) < smallNumber && Math.abs(b13) < smallNumber) || (Math.abs(a13) < smallNumber && Math.abs(b13) < smallNumber)) { // // points too close so set to default circle x = 0; y = 0; rad = 0; } else { // everything is acceptable do the y calculation y = (a13 * c23 - a23 * c13) / (a13 * b23 - a23 * b13); // x calculation // choose best formula for calculation if (Math.abs(a13) > Math.abs(a23)) { x = (c13 - b13 * y) / a13; } else { x = (c23 - b23 * y) / a23; } // radius calculation rad = Math.sqrt((x - p1.x) * (x - p1.x) + (y - p1.y) * (y - p1.y)); } this.center.x = x; this.center.y = y; this.radius = rad; } public Coordinate getCenter() { return this.center; } public double getRadius() { return this.radius; } /** * Given 2 points defining an arc on the circle, interpolates the circle * into a collection of points that provide connected chords that * approximate the arc based on the tolerance value. The tolerance value * specifies the maximum distance between a chord and the circle. * * @param x1 x coordinate of point 1 * @param y1 y coordinate of point 1 * @param x2 x coordinate of point 2 * @param y2 y coordinate of point 2 * @param x3 x coordinate of point 3 * @param y3 y coordinate of point 3 * @param tolerence maximum distance between the center of the chord and the outer * edge of the circle * @return an ordered list of Coordinates representing a series of chords * approximating the arc. */ public static Coordinate[] linearizeArc(double x1, double y1, double x2, double y2, double x3, double y3, double tolerence) { Coordinate p1 = new Coordinate(x1, y1); Coordinate p2 = new Coordinate(x2, y2); Coordinate p3 = new Coordinate(x3, y3); return new Circle(p1, p2, p3).linearizeArc(p1, p2, p3, tolerence); } /** * Given 2 points defining an arc on the circle, interpolates the circle * into a collection of points that provide connected chords that * approximate the arc based on the tolerance value. This method uses a * tolerence value of 1/100 of the length of the radius. * * @param x1 x coordinate of point 1 * @param y1 y coordinate of point 1 * @param x2 x coordinate of point 2 * @param y2 y coordinate of point 2 * @param x3 x coordinate of point 3 * @param y3 y coordinate of point 3 * @return an ordered list of Coordinates representing a series of chords * approximating the arc. */ public static Coordinate[] linearizeArc(double x1, double y1, double x2, double y2, double x3, double y3) { Coordinate p1 = new Coordinate(x1, y1); Coordinate p2 = new Coordinate(x2, y2); Coordinate p3 = new Coordinate(x3, y3); Circle c = new Circle(p1, p2, p3); double tolerence = 0.01 * c.getRadius(); return c.linearizeArc(p1, p2, p3, tolerence); } /** * Given a circle defined by the 3 points, creates a linearized * interpolation of the circle starting and ending on the first coordinate. * This method uses a tolerence value of 1/100 of the length of the radius. * * @param x1 x coordinate of point 1 * @param y1 y coordinate of point 1 * @param x2 x coordinate of point 2 * @param y2 y coordinate of point 2 * @param x3 x coordinate of point 3 * @param y3 y coordinate of point 3 * @return an ordered list of Coordinates representing a series of chords * approximating the arc. */ public static Coordinate[] linearizeCircle(double x1, double y1, double x2, double y2, double x3, double y3) { Coordinate p1 = new Coordinate(x1, y1); Coordinate p2 = new Coordinate(x2, y2); Coordinate p3 = new Coordinate(x3, y3); Circle c = new Circle(p1, p2, p3); double tolerence = 0.01 * c.getRadius(); return c.linearizeArc(p1, p2, p1, tolerence); } /** * Given a circle defined by the 3 points, creates a linearized * interpolation of the circle starting and ending on the first coordinate. * * @param p1 coordinate on the circle * @param p2 another coordinate on the circle * @param p3 yet another coordinate on the circle * @param tolerance maximum distance between the center of the chord and the outer * edge of the circle * @return an ordered list of Coordinates representing a series of chords * approximating the circle. */ public static Coordinate[] linearizeCircle(Coordinate p1, Coordinate p2, Coordinate p3, double tolerance) { Circle c = new Circle(p1, p2, p3); return c.linearizeArc(p1, p2, p1, tolerance); } /** * Given 2 points defining an arc on the circle, interpolates the circle * into a collection of points that provide connected chords that * approximate the arc based on the tolerance value. The tolerance value * specifies the maximum distance between a chord and the circle. * * @param p1 begin coordinate of the arc * @param p2 any other point on the arc * @param p3 end coordinate of the arc * @param tolerence maximum distance between the center of the chord and the outer * edge of the circle * @return an ordered list of Coordinates representing a series of chords * approximating the arc. */ public Coordinate[] linearizeArc(Coordinate p1, Coordinate p2, Coordinate p3, double tolerence) { Arc arc = createArc(p1, p2, p3); List<Coordinate> result = linearizeInternal(null, arc, tolerence); return result.toArray(new Coordinate[result.size()]); } private List<Coordinate> linearizeInternal(List<Coordinate> coordinates, Arc arc, double tolerence) { if (coordinates == null) { coordinates = new ArrayList<Coordinate>(); } double arcHt = arc.getArcHeight(); if (Double.compare(arcHt, tolerence) <= 0 || Double.isNaN(arcHt) || Double.isInfinite(arcHt)) { int lastIndex = coordinates.size() - 1; Coordinate lastCoord = lastIndex >= 0 ? coordinates.get(lastIndex) : null; if (lastCoord == null || !arc.getP1().equals2D(lastCoord)) { coordinates.add(arc.getP1()); coordinates.add(arc.getP2()); } else { coordinates.add(arc.getP2()); } } else { // otherwise, split Arc[] splits = arc.split(); linearizeInternal(coordinates, splits[0], tolerence); linearizeInternal(coordinates, splits[1], tolerence); } return coordinates; } public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } Circle circle = (Circle) o; if (Double.compare(circle.radius, this.radius) != 0) { return false; } if (this.center != null ? !this.center.equals2D(circle.center) : circle.center != null) { return false; } return true; } public String toString() { return "Circle with Radius = " + this.radius + " and a center at the coordinates (" + this.center.x + ", " + this.center.y + ")"; } /** * Returns the angle of the point from the center and the horizontal line * from the center. * * @param p a point in space * @return The angle of the point from the center of the circle */ public double getAngle(Coordinate p) { double dx = p.x - this.center.x; double dy = p.y - this.center.y; double angle; if (dx == 0.0) { if (dy == 0.0) { angle = 0.0; } else if (dy > 0.0) { angle = Math.PI / 2.0; } else { angle = (Math.PI * 3.0) / 2.0; } } else if (dy == 0.0) { if (dx > 0.0) { angle = 0.0; } else { angle = Math.PI; } } else { if (dx < 0.0) { angle = Math.atan(dy / dx) + Math.PI; } else if (dy < 0.0) { angle = Math.atan(dy / dx) + (2 * Math.PI); } else { angle = Math.atan(dy / dx); } } return angle; } public Coordinate getPoint(final double angle) { double x = Math.cos(angle) * this.radius; x = x + this.center.x; x = this.precisionModel.makePrecise(x); double y = Math.sin(angle) * this.radius; y = y + this.center.y; y = this.precisionModel.makePrecise(y); return new Coordinate(x, y); } /** * @param p A point in space * @return The distance the point is from the center of the circle */ public double distanceFromCenter(Coordinate p) { return Math.abs(this.center.distance(p)); } public Arc createArc(Coordinate p1, Coordinate p2, Coordinate p3) { return new Arc(p1, p2, p3); } /** * Returns an angle between 0 and 2*PI. For example, 4*PI would get returned * as 2*PI since they are equivalent. * * @param angle an angle in radians to normalize * @return an angle between 0 and 2*PI */ public static double normalizeAngle(double angle) { double maxRadians = 2 * Math.PI; if (angle >= 0 && angle <= maxRadians) { return angle; } if (angle < 0) { return maxRadians - Math.abs(angle); } else { return angle % maxRadians; } } /** * Returns the angle between the angles a1 and a2 in radians. Angle is * calculated in the counterclockwise direction. * * @param a1 first angle * @param a2 second angle * @return the angle between a1 and a2 in the clockwise direction */ public static double subtractAngles(double a1, double a2) { if (a1 < a2) { return a2 - a1; } else { return TWO_PI - Math.abs(a2 - a1); } } private static final double TWO_PI = Math.PI * 2; public class Arc { private Coordinate p1, p2; private double arcAngle; // angle in radians private double p1Angle; private double p2Angle; private boolean clockwise; private Arc(Coordinate p1, Coordinate midPt, Coordinate p2) { this.p1 = p1; this.p2 = p2; this.p1Angle = getAngle(p1); // See if this arc covers the whole circle if (p1.equals2D(p2)) { this.p2Angle = TWO_PI + this.p1Angle; this.arcAngle = TWO_PI; } else { this.p2Angle = getAngle(p2); double midPtAngle = getAngle(midPt); // determine the direction double ccDegrees = Circle.subtractAngles(this.p1Angle, midPtAngle) + Circle.subtractAngles(midPtAngle, this.p2Angle); if (ccDegrees < TWO_PI) { this.clockwise = false; this.arcAngle = ccDegrees; } else { this.clockwise = true; this.arcAngle = TWO_PI - ccDegrees; } } } private Arc(Coordinate p1, Coordinate p2, boolean isClockwise) { this.p1 = p1; this.p2 = p2; this.clockwise = isClockwise; this.p1Angle = getAngle(p1); if (p1.equals2D(p2)) { this.p2Angle = TWO_PI + this.p1Angle; } else { this.p2Angle = getAngle(p2); } determineArcAngle(); } private void determineArcAngle() { double diff; if (this.p1.equals2D(this.p2)) { diff = TWO_PI; } else if (this.clockwise) { diff = this.p1Angle - this.p2Angle; } else { diff = this.p2Angle - this.p1Angle; } this.arcAngle = Circle.normalizeAngle(diff); } /** * given a an arc defined from p1 to p2 existing on this circle, returns * the height of the arc. This height is defined as the distance from * the center of a chord defined by (p1, p2) and the outer edge of the * circle. * * @return the arc height */ public double getArcHeight() { Coordinate chordCenterPt = this.getChordCenterPoint(); double dist = distanceFromCenter(chordCenterPt); if (this.arcAngle > Math.PI) { return Circle.this.radius + dist; } else { return Circle.this.radius - dist; } } public Coordinate getChordCenterPoint() { double centerX = this.p1.x + (this.p2.x - this.p1.x) / 2; double centerY = this.p1.y + (this.p2.y - this.p1.y) / 2; return new Coordinate(centerX, centerY); } public Arc[] split() { int directionFactor = isClockwise() ? -1 : 1; double angleOffset = directionFactor * (this.arcAngle / 2); double midAngle = this.p1Angle + angleOffset; Coordinate newMidPoint = getPoint(midAngle); Arc arc1 = new Arc(this.p1, newMidPoint, isClockwise()); Arc arc2 = new Arc(newMidPoint, this.p2, isClockwise()); return new Arc[]{arc1, arc2}; } public Coordinate getP1() { return this.p1; } public Coordinate getP2() { return this.p2; } public double getArcAngle() { return this.arcAngle; } public double getArcAngleDegrees() { return Math.toDegrees(this.arcAngle); } public double getP1Angle() { return this.p1Angle; } public double getP2Angle() { return this.p2Angle; } public boolean isClockwise() { return this.clockwise; } public String toString() { return "P1: " + this.p1 + " P2: " + this.p2 + " clockwise: " + this.clockwise; } } }