Skip to content

Commit

Permalink
Add Angle::sinCos to avoid small errors, e.g. with buffer operations
Browse files Browse the repository at this point in the history
  • Loading branch information
mwtoews committed Nov 9, 2023
1 parent 03f3ac7 commit 5ad6a79
Show file tree
Hide file tree
Showing 5 changed files with 90 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}
}
Expand All @@ -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();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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]);
Expand Down Expand Up @@ -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);
Expand All @@ -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;
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down

0 comments on commit 5ad6a79

Please sign in to comment.