diff --git a/modules/core/src/main/java/org/locationtech/jts/algorithm/Angle.java b/modules/core/src/main/java/org/locationtech/jts/algorithm/Angle.java index 1da839b395..6d26092b3f 100644 --- a/modules/core/src/main/java/org/locationtech/jts/algorithm/Angle.java +++ b/modules/core/src/main/java/org/locationtech/jts/algorithm/Angle.java @@ -313,11 +313,33 @@ public static double diff(double ang1, double ang2) { } if (delAngle > Math.PI) { - delAngle = (2 * Math.PI) - delAngle; + delAngle = PI_TIMES_2 - delAngle; } return delAngle; } + + /** + * Computes both sin and cos of an angle. + * + * The angle does not need to be normalized. Unlike Math.sin + * and Math.cos, this method will clip near-zero values to zero + * for (e.g.) sin(pi) and cos(pi/2). + * + * @param ang the input angle (in radians) + * @return an array with the results of [sin(ang), cos(ang)] + */ + public static double[] sinCos(double ang) { + double[] sincos = new double[2]; + // calculate both + sincos[0] = Math.sin(ang); + sincos[1] = Math.cos(ang); + // clip near zero values + if (Math.abs(sincos[0]) < 5e-16) sincos[0] = 0.0; + if (Math.abs(sincos[1]) < 5e-16) sincos[1] = 0.0; + return sincos; + } + /** * Projects a point by a given angle and distance. @@ -328,8 +350,9 @@ public static double diff(double ang1, double ang2) { * @return the projected point */ public static Coordinate project(Coordinate p, double angle, double dist) { - double x = p.getX() + dist * Math.cos(angle); - double y = p.getY() + dist * Math.sin(angle); + double[] sincos = Angle.sinCos(angle); + double x = p.getX() + dist * sincos[1]; + double y = p.getY() + dist * sincos[0]; return new Coordinate(x, y); } } diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/buffer/BufferParameters.java b/modules/core/src/main/java/org/locationtech/jts/operation/buffer/BufferParameters.java index e9cc6f8799..f4799fc26b 100644 --- a/modules/core/src/main/java/org/locationtech/jts/operation/buffer/BufferParameters.java +++ b/modules/core/src/main/java/org/locationtech/jts/operation/buffer/BufferParameters.java @@ -11,6 +11,8 @@ */ package org.locationtech.jts.operation.buffer; +import org.locationtech.jts.algorithm.Angle; + /** * A value class containing the parameters which * specify how a buffer should be constructed. @@ -176,7 +178,7 @@ public void setQuadrantSegments(int quadSegs) */ public static double bufferDistanceError(int quadSegs) { - double alpha = Math.PI / 2.0 / quadSegs; + double alpha = Angle.PI_OVER_2 / quadSegs; return 1 - Math.cos(alpha / 2.0); } diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/buffer/OffsetSegmentGenerator.java b/modules/core/src/main/java/org/locationtech/jts/operation/buffer/OffsetSegmentGenerator.java index f1591e4451..2104b8ad89 100644 --- a/modules/core/src/main/java/org/locationtech/jts/operation/buffer/OffsetSegmentGenerator.java +++ b/modules/core/src/main/java/org/locationtech/jts/operation/buffer/OffsetSegmentGenerator.java @@ -114,7 +114,7 @@ public OffsetSegmentGenerator(PrecisionModel precisionModel, int quadSegs = bufParams.getQuadrantSegments(); if (quadSegs < 1) quadSegs = 1; - filletAngleQuantum = Math.PI / 2.0 / quadSegs; + filletAngleQuantum = Angle.PI_OVER_2 / quadSegs; /** * Non-round joins cause issues with short closing segments, so don't use @@ -428,7 +428,7 @@ public void addLineEndCap(Coordinate p0, Coordinate p1) case BufferParameters.CAP_ROUND: // add offset seg points with a fillet between them segList.addPt(offsetL.p1); - addDirectedFillet(p1, angle + Math.PI / 2, angle - Math.PI / 2, Orientation.CLOCKWISE, distance); + addDirectedFillet(p1, angle + Angle.PI_OVER_2, angle - Angle.PI_OVER_2, Orientation.CLOCKWISE, distance); segList.addPt(offsetR.p1); break; case BufferParameters.CAP_FLAT: @@ -439,8 +439,9 @@ public void addLineEndCap(Coordinate p0, Coordinate p1) case BufferParameters.CAP_SQUARE: // add a square defined by extensions of the offset segment endpoints Coordinate squareCapSideOffset = new Coordinate(); - squareCapSideOffset.x = Math.abs(distance) * Math.cos(angle); - squareCapSideOffset.y = Math.abs(distance) * Math.sin(angle); + double[] sincos = Angle.sinCos(angle); + squareCapSideOffset.x = Math.abs(distance) * sincos[1]; + squareCapSideOffset.y = Math.abs(distance) * sincos[0]; Coordinate squareCapLOffset = new Coordinate( offsetL.p1.x + squareCapSideOffset.x, @@ -534,7 +535,7 @@ private void addLimitedMitreJoin( Coordinate bevelMidPt = project(cornerPt, -mitreLimitDistance, dirBisector); // direction of bevel segment (at right angle to corner bisector) - double dirBevel = Angle.normalize(dirBisector + Math.PI/2.0); + double dirBevel = Angle.normalize(dirBisector + Angle.PI_OVER_2); // compute the candidate bevel segment by projecting both sides of the midpoint Coordinate bevel0 = project(bevelMidPt, distance, dirBevel); @@ -567,8 +568,9 @@ private void addLimitedMitreJoin( * @return the projected point */ private static Coordinate project(Coordinate pt, double d, double dir) { - double x = pt.x + d * Math.cos(dir); - double y = pt.y + d * Math.sin(dir); + double[] sincos = Angle.sinCos(dir); + double x = pt.x + d * sincos[1]; + double y = pt.y + d * sincos[0]; return new Coordinate(x, y); } @@ -607,10 +609,10 @@ private void addCornerFillet(Coordinate p, Coordinate p0, Coordinate p1, int dir double endAngle = Math.atan2(dy1, dx1); if (direction == Orientation.CLOCKWISE) { - if (startAngle <= endAngle) startAngle += 2.0 * Math.PI; + if (startAngle <= endAngle) startAngle += Angle.PI_TIMES_2; } else { // direction == COUNTERCLOCKWISE - if (startAngle >= endAngle) startAngle -= 2.0 * Math.PI; + if (startAngle >= endAngle) startAngle -= Angle.PI_TIMES_2; } segList.addPt(p0); addDirectedFillet(p, startAngle, endAngle, direction, radius); @@ -640,9 +642,9 @@ private void addDirectedFillet(Coordinate p, double startAngle, double endAngle, Coordinate pt = new Coordinate(); for (int i = 0; i < nSegs; i++) { - double angle = startAngle + directionFactor * i * angleInc; - pt.x = p.x + radius * Math.cos(angle); - pt.y = p.y + radius * Math.sin(angle); + double[] sincos = Angle.sinCos(startAngle + directionFactor * i * angleInc); + pt.x = p.x + radius * sincos[1]; + pt.y = p.y + radius * sincos[0];; segList.addPt(pt); } } @@ -655,7 +657,7 @@ public void createCircle(Coordinate p) // add start point Coordinate pt = new Coordinate(p.x + distance, p.y); segList.addPt(pt); - addDirectedFillet(p, 0.0, 2.0 * Math.PI, -1, distance); + addDirectedFillet(p, 0.0, Angle.PI_TIMES_2, -1, distance); segList.closeRing(); } diff --git a/modules/core/src/main/java/org/locationtech/jts/util/GeometricShapeFactory.java b/modules/core/src/main/java/org/locationtech/jts/util/GeometricShapeFactory.java index 7ba7dd2ec9..3404f0c1a9 100644 --- a/modules/core/src/main/java/org/locationtech/jts/util/GeometricShapeFactory.java +++ b/modules/core/src/main/java/org/locationtech/jts/util/GeometricShapeFactory.java @@ -11,6 +11,7 @@ */ package org.locationtech.jts.util; +import org.locationtech.jts.algorithm.Angle; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.Envelope; import org.locationtech.jts.geom.Geometry; @@ -221,9 +222,9 @@ public Polygon createEllipse() Coordinate[] pts = new Coordinate[nPts + 1]; int iPt = 0; for (int i = 0; i < nPts; i++) { - double ang = i * (2 * Math.PI / nPts); - double x = xRadius * Math.cos(ang) + centreX; - double y = yRadius * Math.sin(ang) + centreY; + double[] sincos = Angle.sinCos(i * Angle.PI_TIMES_2 / nPts); + double x = xRadius * sincos[1] + centreX; + double y = yRadius * sincos[0] + centreY; pts[iPt++] = coord(x, y); } pts[iPt] = new Coordinate(pts[0]); @@ -319,16 +320,16 @@ public LineString createArc( double centreY = env.getMinY() + yRadius; double angSize = angExtent; - if (angSize <= 0.0 || angSize > 2 * Math.PI) - angSize = 2 * Math.PI; + if (angSize <= 0.0 || angSize > Angle.PI_TIMES_2) + angSize = Angle.PI_TIMES_2; double angInc = angSize / (nPts - 1); Coordinate[] pts = new Coordinate[nPts]; int iPt = 0; for (int i = 0; i < nPts; i++) { - double ang = startAng + i * angInc; - double x = xRadius * Math.cos(ang) + centreX; - double y = yRadius * Math.sin(ang) + centreY; + double[] sincos = Angle.sinCos(startAng + i * angInc); + double x = xRadius * sincos[1] + centreX; + double y = yRadius * sincos[0] + centreY; pts[iPt++] = coord(x, y); } LineString line = geomFact.createLineString(pts); @@ -353,8 +354,8 @@ public Polygon createArcPolygon(double startAng, double angExtent) { double centreY = env.getMinY() + yRadius; double angSize = angExtent; - if (angSize <= 0.0 || angSize > 2 * Math.PI) - angSize = 2 * Math.PI; + if (angSize <= 0.0 || angSize > Angle.PI_TIMES_2) + angSize = Angle.PI_TIMES_2; double angInc = angSize / (nPts - 1); // double check = angInc * nPts; // double checkEndAng = startAng + check; @@ -364,10 +365,9 @@ public Polygon createArcPolygon(double startAng, double angExtent) { int iPt = 0; pts[iPt++] = coord(centreX, centreY); for (int i = 0; i < nPts; i++) { - double ang = startAng + angInc * i; - - double x = xRadius * Math.cos(ang) + centreX; - double y = yRadius * Math.sin(ang) + centreY; + double[] sincos = Angle.sinCos(startAng + angInc * i); + double x = xRadius * sincos[1] + centreX; + double y = yRadius * sincos[0] + centreY; pts[iPt++] = coord(x, y); } pts[iPt++] = coord(centreX, centreY); diff --git a/modules/core/src/test/java/org/locationtech/jts/algorithm/AngleTest.java b/modules/core/src/test/java/org/locationtech/jts/algorithm/AngleTest.java index 417bb1f538..2a22ec6814 100644 --- a/modules/core/src/test/java/org/locationtech/jts/algorithm/AngleTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/algorithm/AngleTest.java @@ -159,6 +159,38 @@ public void testAngleBisector() { assertEquals(45, Math.toDegrees(Angle.bisector(p(13,10), p(10,10), p(10,20))), 0.01); } + public void testSinCos() { + + // -720 to 720 degrees with 1 degree increments + for (int angdeg = -720; angdeg <= 720; angdeg++) { + double ang = Angle.toRadians(angdeg); + + double[] sincos = Angle.sinCos(ang); + + double cSin = Math.sin(ang); + double cCos = Math.cos(ang); + if ( (angdeg % 90) == 0 ) { + // not always the same for multiples of 90 degrees + assertTrue(Math.abs(sincos[0] - cSin) < 1e-15); + assertTrue(Math.abs(sincos[1] - cCos) < 1e-15); + } else { + assertEquals(sincos[0], cSin); + assertEquals(sincos[1], cCos); + } + + } + + // use radian increments that don't snap to exact degrees or zero + for (double angrad = -6.3; angrad < 6.3; angrad += 0.013) { + + double[] sincos = Angle.sinCos(angrad); + + assertEquals(sincos[0], Math.sin(angrad)); + assertEquals(sincos[1], Math.cos(angrad)); + + } +} + private static Coordinate p(double x, double y) { return new Coordinate(x, y); }