This is an automated email from the ASF dual-hosted git repository.
desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git
The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
new 09fa9bf Try harder to avoid unreasonably long chain of Bézier curves.
First implementation of createCircularRegion2D(…), not yet working because of
infinite derivative values.
09fa9bf is described below
commit 09fa9bf7e972ac665e9a07b3ca63fcb4a62e2b2d
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Wed May 22 20:48:12 2019 +0200
Try harder to avoid unreasonably long chain of Bézier curves.
First implementation of createCircularRegion2D(…), not yet working because
of infinite derivative values.
---
.../internal/referencing/PositionTransformer.java | 18 ++
.../sis/internal/referencing/j2d/Bezier.java | 218 ++++++++++++++-------
.../apache/sis/referencing/GeodeticCalculator.java | 173 +++++++++++++---
.../sis/referencing/GeodeticCalculatorTest.java | 88 ++++++---
ide-project/NetBeans/nbproject/genfiles.properties | 2 +-
ide-project/NetBeans/nbproject/project.xml | 1 +
6 files changed, 375 insertions(+), 125 deletions(-)
diff --git
a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/PositionTransformer.java
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/PositionTransformer.java
index 87c2661..6ad5976 100644
---
a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/PositionTransformer.java
+++
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/PositionTransformer.java
@@ -181,6 +181,24 @@ public final class PositionTransformer extends
GeneralDirectPosition {
}
/**
+ * Transforms the given position from the CRS of this position to the
default CRS.
+ * The result is stored in the given array.
+ *
+ * @param point the coordinates of the point to transform in-place.
+ * @throws TransformException if a coordinate transformation was required
and failed.
+ */
+ public void transform(final double[] point) throws TransformException {
+ if (point != null) {
+ if (lastCRS != defaultCRS) {
+ setSourceCRS(defaultCRS);
+ }
+ if (forward != null) {
+ forward.transform(point, 0, point, 0, 1);
+ }
+ }
+ }
+
+ /**
* Transforms a given position from its CRS to the CRS of this {@code
PositionTransformer}.
* If the CRS associated to the given position is {@code null}, then that
CRS is assumed to
* be the default CRS specified at construction time. Otherwise if that
CRS is not equal to
diff --git
a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/j2d/Bezier.java
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/j2d/Bezier.java
index 9ea2294..1c9d580 100644
---
a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/j2d/Bezier.java
+++
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/j2d/Bezier.java
@@ -17,6 +17,9 @@
package org.apache.sis.internal.referencing.j2d;
import java.awt.geom.Path2D;
+import java.awt.geom.Line2D;
+import java.awt.geom.QuadCurve2D;
+import java.awt.geom.CubicCurve2D;
import org.opengis.referencing.operation.TransformException;
import static java.lang.Math.abs;
@@ -24,7 +27,17 @@ import static java.lang.Double.isInfinite;
/**
- * Helper class for appending Bézier curves to a path.
+ * Helper class for appending Bézier curves to a path. This class computes
cubic Bézier from 3 points and two slopes.
+ * The three points are the start point (t=0), middle point (t=½) and end
point (t=1). The slopes are at start point
+ * and end point. The Bézier curve will obey exactly to those conditions (up
to rounding errors).
+ *
+ * <p>After creating each Bézier curve, this class performs a quality control
using 2 additional points located at
+ * one quarter (t≈¼) and three quarters (t≈¾) of the curve. If the distance
between given points and a close point
+ * on the curve is greater than {@linkplain #εx} and {@linkplain #εy}
thresholds, then the curve is divided in two
+ * smaller curves and the process is repeated until curves meet the quality
controls.</p>
+ *
+ * <p>If a quadratic curve degenerates to a cubic curve or a straight line,
ignoring errors up to {@linkplain #εx}
+ * and {@linkplain #εy}, then {@link CubicCurve2D} are replaced by {@link
QuadCurve2D} or {@link Line2D}.</p>
*
* @author Martin Desruisseaux (Geomatys)
* @version 1.0
@@ -36,14 +49,22 @@ import static java.lang.Double.isInfinite;
*/
public abstract class Bezier {
/**
+ * Limit the maximal depth for avoiding shapes of unreasonable size.
+ *
+ * @see #depth
+ */
+ private static final int DEPTH_LIMIT = 10;
+
+ /**
* The path where to append Bézier curves.
*/
private final Path2D path;
/**
- * Maximal distance on <var>x</var> and <var>y</var> axis between the
cubic Bézier curve and quadratic
- * or linear simplifications. Initial value is zero. Subclasses should set
the values either in their
- * constructor or at {@link #evaluateAt(double)} method call.
+ * Maximal distance (approximate) on <var>x</var> and <var>y</var> axis
between the cubic Bézier curve and quadratic
+ * or linear simplifications. Initial value is zero. Subclasses should set
the values either in their constructor or
+ * at {@link #evaluateAt(double)} method call. Can be set to infinity or
NaN for disabling the quality checks, or to
+ * a negative value for forcing an unconditional division of a Bézier
curve in two more curves.
*/
protected double εx, εy;
@@ -60,6 +81,13 @@ public abstract class Bezier {
protected final double[] point;
/**
+ * The number of times a curve has been divided in smaller curves.
+ *
+ * @see #DEPTH_LIMIT
+ */
+ protected int depth;
+
+ /**
* Creates a new builder.
*
* @param dimension length of the {@link #point} array. Must be at least
2.
@@ -91,6 +119,26 @@ public abstract class Bezier {
protected abstract double evaluateAt(double t) throws TransformException;
/**
+ * Returns whether we should accept the given coordinates. This method is
invoked when this {@code Bezier} class thinks
+ * that the given point is not valid, but can not said for sure because
computing an exact answer would be expansive
+ * (because of the difficulty to map Bézier parameter <var>t</var>=¼ and ¾
to a distance on the curve).
+ *
+ * <p>The default implementation returns always {@code false}, since this
method is invoked only when this class
+ * thinks that the point is not valid.</p>
+ *
+ * @param x first coordinate value of a point on the curve to evaluate
for fitness.
+ * @param y second coordinate value of a point on the curve to evaluate
for fitness.
+ * @return whether the given point is close enough to a valid point.
+ * @throws TransformException if a coordinate operations was required and
failed.
+ *
+ * @todo This method is a hack. We should replace it by a computation of
the Bézier <var>t</var> parameter
+ * of the point closest to the given (x₁,y₁) and (x₃,y₃) points.
+ */
+ protected boolean isValid(double x, double y) throws TransformException {
+ return false;
+ }
+
+ /**
* Creates a sequence of Bézier curves from the position given by {@code
evaluateAt(0)} to the position given
* by {@code evaluateAt(0)}. This method determines the number of
intermediate points required for achieving
* the precision requested by the {@code εx} and {@code εy} parameters
given at construction time.
@@ -140,20 +188,24 @@ public abstract class Bezier {
final double tx = εx;
final double ty = εy;
final double α1 = evaluateAt(0.75*tmin + 0.25*tmax); //
`point` become (x₁,y₁) with this method call.
+ final double xi = point[0]; //
Must be after the call to `evaluateAt(t₁)`.
+ final double yi = point[1];
if (tx < εx) εx = tx; //
Take smallest tolerance values.
if (ty < εy) εy = ty;
- if (curve(point[0], point[1], x2, y2, x3, y3, x4, y4, α4)) { //
Must be after the call to `evaluateAt(t₁)`.
+ if (curve(xi, yi, x2, y2, x3, y3, x4, y4, α4)) {
x0 = x4;
y0 = y4;
α0 = α4;
} else {
+ depth++;
final double εxo = εx;
final double εyo = εy;
final double th = 0.5 * (tmin + tmax);
- sequence(tmin, th, point[0], point[1], α1, x2, y2, α2);
- sequence(th, tmax, x3, y3, α3, x4, y4, α4);
+ sequence(tmin, th, xi, yi, α1, x2, y2, α2);
+ sequence(th, tmax, x3, y3, α3, x4, y4, α4);
εx = εxo;
εy = εyo;
+ depth--;
}
}
@@ -175,9 +227,9 @@ public abstract class Bezier {
* If the curve is not accurate enough, then this method does nothing and
return {@code false}. In that case, the caller
* should split the curve in two smaller parts and invoke this method
again.</p>
*
- * <p>If the full equation is required for representing the curve, then
this method appends a {@link java.awt.geom.CubicCurve2D}.
- * If the same curve can be represented by a quadratic curve, then this
method appends a {@link java.awt.geom.QuadCurve2D}.
- * If the curve is actually a straight line, then this method appends a
{@link java.awt.geom.Line2D}.</p>
+ * <p>If the full equation is required for representing the curve, then
this method appends a {@link CubicCurve2D}.
+ * If the same curve can be represented by a quadratic curve, then this
method appends a {@link QuadCurve2D}.
+ * If the curve is actually a straight line, then this method appends a
{@link Line2D}.</p>
*
* @param x1 <var>x</var> coordinate at <var>t</var>≈¼ (quality control,
may be NaN).
* @param y1 <var>y</var> coordinate at <var>t</var>≈¼ (quality control,
may be NaN).
@@ -193,7 +245,8 @@ public abstract class Bezier {
private boolean curve(double x1, double y1,
double x2, double y2,
double x3, double y3,
- final double x4, final double y4, final double α4)
+ final double x4, final double y4,
+ final double α4) throws TransformException
{
/*
* Equations in this method are simplified as if (x₀,y₀) coordinates
are (0,0).
@@ -224,11 +277,13 @@ public abstract class Bezier {
* they are not used in Line2D computation. This allows
`evaluateAt(t)` method to return NaN if it can
* not provide a point for a given `t` value.
*/
- final double slope = Δy / Δx;
- if ((!isVertical && (abs(x2*slope - y2) > εy || abs(x1*slope
- y1) > εy || abs(x3*slope - y3) > εy)) ||
- (!isHorizontal && (abs(y2/slope - x2) > εx || abs(y1/slope
- x1) > εx || abs(y3/slope - x3) > εx)))
- {
- return false;
+ if (depth < DEPTH_LIMIT) {
+ final double slope = Δy / Δx;
+ if ((!isVertical && (abs(x2*slope - y2) > εy ||
abs(x1*slope - y1) > εy || abs(x3*slope - y3) > εy)) ||
+ (!isHorizontal && (abs(y2/slope - x2) > εx ||
abs(y1/slope - x1) > εx || abs(y3/slope - x3) > εx)))
+ {
+ return false;
+ }
}
path.lineTo(x4, y4);
return true;
@@ -281,66 +336,74 @@ public abstract class Bezier {
* If any of (x₁,y₁) and (x₃,y₃) coordinates is NaN, then this method
accepts the curve as valid.
* This allows `evaluateAt(t)` to return NaN if it can not provide a
point for a given `t` value.
*/
- double xi = 27./64*ax + 9./64*bx + 1./64*Δx; // "xi" is for "x
interpolated (on curve)".
- double yi = 27./64*ay + 9./64*by + 1./64*Δy;
- if (abs(xi - x1) > εx || abs(yi - y1) > εy) {
+ if (depth < DEPTH_LIMIT) {
+ double xi = 27./64*ax + 9./64*bx + 1./64*Δx; // "xi" is for
"x interpolated (on curve)".
+ double yi = 27./64*ay + 9./64*by + 1./64*Δy;
+ if (abs(xi - x1) > εx || abs(yi - y1) > εy) {
+ /*
+ * Above code tested (x,y) coordinates at t=¼ exactly (we will
test t=¾ later). However this t value does not
+ * necessarily correspond to one quarter of the distance,
because the speed at which t varies is not the same
+ * than the speed at which Bézier curve length increases.
Unfortunately computing the t values at a given arc
+ * length is complicated. We tested an approach based on
computing the y value on the curve for a given x value
+ * by starting from the Bézier curve equation:
+ *
+ * x(t) = x₀(1-t)³ + 3aₓ(1-t)²t + 3bₓ(1-t)t² + x₄t³
+ *
+ * rearranged as:
+ *
+ * (-x₀ + 3aₓ - 3bₓ + x₄)t³ + (3x₀ - 6aₓ + 3bₓ)t² + (-3x₀
+ 3aₓ)t + x₀ - x = 0
+ *
+ * and finding the roots with the CubicCurve2D.solveCubic(…)
method. However the results were worst than using
+ * fixed t values. If we want to improve on that in a future
version, we would need a function for computing
+ * arc length (for example based on
https://pomax.github.io/bezierinfo/#arclength), then use iterative method
+ * like
https://www.geometrictools.com/Documentation/MovingAlongCurveSpecifiedSpeed.pdf
(retrieved May 2019).
+ *
+ * Instead we perform another test using the tangent of the
curve at point P₁ (and later P₃).
+ *
+ * x′(t) = 3(1-t)²(aₓ - x₀) + 6t(1-t)(bₓ - aₓ) + 3t²(x₄ -
bₓ) and same for y′(t).
+ *
+ * The derivatives give us a straight line (the tangent) as an
approximation of the curve around P₁.
+ * We can then compute the point on that line which is nearest
to P₁. It should be close to current
+ * (xi,yi) coordinates, but may vary a little bit.
+ */
+ double slope = (27./16*ay + 18./16*(by-ay) + 3./16*(y4-by))
+ / (27./16*ax + 18./16*(bx-ax) + 3./16*(x4-bx));
// ∂y/∂x at t=¼.
+ double offset = (yi - slope*xi);
// Value of y at x=0.
+ xi = ((y1 - offset) * slope + x1) / (slope*slope + 1);
// NaN if slope is infinite.
+ yi = offset + xi*slope;
// Closer (xi,yi) coordinates.
+ if (abs(xi - x1) > εx || abs(yi - y1) > εy) {
+ if (!isValid(xi + x0, yi + y0)) return false;
+ }
+ /*
+ * At this point we consider (x₁,y₁) close enough even if the
initial test considered it too far.
+ * This decision is based on the assumption that the straight
line is an approximation good enough
+ * in the vicinity of P₁. We did not verified that assumption.
If we want to improve on that in a
+ * future version, we could use the second derivative:
+ *
+ * x″(t) = 6(1-t)(x₀ - 2aₓ + bₓ) + 6t(aₓ - 2bₓ + x₄)
and same for y″(t).
+ *
+ * Applying chain rule: ∂²y/∂x² = y″/x′² + y′/x″
+ *
+ * We could then estimate the change of slope at the new
(xi,yi) compared to the initial (xi,yi)
+ * and verify that this change is below some threshold. We do
not perform that check for now on
+ * the assumption that the Bézier curve is smooth enough in
the context of map projections.
+ */
+ }
/*
- * Above code tested (x,y) coordinates at t=¼ exactly (we will
test t=¾ later). However this t value does not
- * necessarily correspond to one quarter of the distance, because
the speed at which t varies is not the same
- * than the speed at which Bézier curve length increases.
Unfortunately computing the t values at a given arc
- * length is complicated. We tested an approach based on computing
the y value on the curve for a given x value
- * by starting from the Bézier curve equation:
- *
- * x(t) = x₀(1-t)³ + 3a(1-t)²t + 3b(1-t)t² + x₄t³
- *
- * rearranged as:
- *
- * (-x₀ + 3a - 3b + x₄)t³ + (3x₀ - 6a + 3b)t² + (-3x₀ + 3a)t +
x₀ - x = 0
- *
- * and finding the roots with the CubicCurve2D.solveCubic(…)
method. However the results were worst than using
- * fixed t values. If we want to improve on that in a future
version, we would need a function for computing
- * arc length (for example based on
https://pomax.github.io/bezierinfo/#arclength), then use iterative method
- * like
https://www.geometrictools.com/Documentation/MovingAlongCurveSpecifiedSpeed.pdf
(retrieved May 2019).
- *
- * Instead we perform another test using the tangent of the curve
at point P₁ (and later P₃).
- *
- * x′(t) = 3(1-t)²(a-x₀) + 6(1-t)t(b-a) + 3t²(x₄ - b)
and same for y′(t).
- *
- * The derivatives give us a straight line (the tangent) as an
approximation of the curve around P₁.
- * We can then compute the point on that line which is nearest to
P₁. It should be close to current
- * (xi,yi) coordinates, but may vary a little bit.
+ * Same than above, but with point P₁ replaced by P₃ and t=¼
replaced by t=¾.
+ * The change of t value changes the coefficients in formulas
below.
*/
- double slope = (27./16*ay + 18./16*(by-ay) + 3./16*(y4-by))
- / (27./16*ax + 18./16*(bx-ax) + 3./16*(x4-bx));
// ∂y/∂x at t=¼.
- double offset = (yi - slope*xi);
// Value of y at x=0.
- xi = ((y1 - offset) * slope + x1) / (slope*slope + 1);
// Closer (xi,yi) coordinates.
- yi = offset + xi*slope;
- xi = abs(xi - x1);
// At this point (xi,yi) are distances.
- yi = abs(yi - y1);
- if ((xi > εx && xi != Double.POSITIVE_INFINITY) ||
- (yi > εy && yi != Double.POSITIVE_INFINITY))
- {
- return false;
- }
- }
- /*
- * Same than above, but with point P₁ replaced by P₃ and t=¼ replaced
by t=¾.
- * The change of t value changes the coefficients in formulas below.
- */
- xi = 9./64*ax + 27./64*bx + 27./64*Δx;
- yi = 9./64*ay + 27./64*by + 27./64*Δy;
- if (abs(xi - x3) > εx || abs(yi - y3) > εy) {
- double slope = (3./16*ay + 18./16*(by-ay) + 27./16*(y4-by))
- / (3./16*ax + 18./16*(bx-ax) + 27./16*(x4-bx));
- double offset = (yi - slope*xi);
- xi = ((y3 - offset) * slope + x3) / (slope*slope + 1);
- yi = offset + xi*slope;
- xi = abs(xi - x3);
- yi = abs(yi - y3);
- if ((xi > εx && xi != Double.POSITIVE_INFINITY) ||
- (yi > εy && yi != Double.POSITIVE_INFINITY))
- {
- return false;
+ xi = 9./64*ax + 27./64*bx + 27./64*Δx;
+ yi = 9./64*ay + 27./64*by + 27./64*Δy;
+ if (abs(xi - x3) > εx || abs(yi - y3) > εy) {
+ double slope = (3./16*ay + 18./16*(by-ay) + 27./16*(y4-by))
+ / (3./16*ax + 18./16*(bx-ax) + 27./16*(x4-bx));
+ double offset = (yi - slope*xi);
+ xi = ((y3 - offset) * slope + x3) / (slope*slope + 1);
+ yi = offset + xi*slope;
+ if (abs(xi - x3) > εx || abs(yi - y3) > εy) {
+ if (!isValid(xi + x0, yi + y0)) return false;
+ }
}
}
/*
@@ -358,10 +421,13 @@ public abstract class Bezier {
* We compute C both ways and check if they are close enough to each
other:
*
* ΔC = (3⋅(B - A) - (P₄ - P₀))/2
+ *
+ * We multiply tolerance factor by 2 because of moving the quadratic
curve control point by 1 can move the closest
+ * point on the curve by at most ½.
*/
final double Δqx, Δqy;
- if (abs(Δqx = (3*(bx - ax) - Δx)/2) <= εx && // P₀ is zero.
- abs(Δqy = (3*(by - ay) - Δy)/2) <= εy)
+ if (abs(Δqx = (3*(bx - ax) - Δx)/2) <= 2*εx && // P₀ is zero.
+ abs(Δqy = (3*(by - ay) - Δy)/2) <= 2*εy)
{
final double qx = (3*ax + Δqx)/2; // Take average of
2 control points.
final double qy = (3*ay + Δqy)/2;
diff --git
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/GeodeticCalculator.java
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/GeodeticCalculator.java
index 9bce0d0..2ab39b2 100644
---
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/GeodeticCalculator.java
+++
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/GeodeticCalculator.java
@@ -17,6 +17,7 @@
package org.apache.sis.referencing;
import java.awt.Shape;
+import java.awt.geom.Path2D;
import java.util.Locale;
import java.io.IOException;
import java.io.UncheckedIOException;
@@ -628,17 +629,22 @@ public class GeodeticCalculator {
* <li>The last point is {@link #getEndPoint()}, potentially with 360°
added or subtracted to the longitude.</li>
* </ol>
*
+ * This method tries to stay within the given tolerance threshold of the
geodesic track.
+ * The {@code tolerance} parameter should not be too small for avoiding
creation of unreasonably long chain of Bézier curves.
+ * For example a value of 1/10 of geodesic length may be sufficient.
+ *
* <b>Limitations:</b>
* This method depends on the presence of {@code java.desktop} module.
This limitations may be addressed
* in a future Apache SIS version (see <a
href="https://issues.apache.org/jira/browse/SIS-453">SIS-453</a>).
*
* @param tolerance maximal error between the approximated curve and
actual geodesic track
* in the units of measurement given by {@link
#getDistanceUnit()}.
+ * This is approximate; the actual errors may vary
around that value.
* @return an approximation of geodesic track as Bézier curves in a Java2D
object.
* @throws TransformException if the coordinates can not be transformed to
{@linkplain #getPositionCRS() position CRS}.
* @throws IllegalStateException if some required properties have not been
specified.
*/
- public Shape toGeodesicPath2D(final double tolerance) throws
TransformException {
+ public Shape createGeodesicPath2D(final double tolerance) throws
TransformException {
ArgumentChecks.ensureStrictlyPositive("tolerance", tolerance);
if (isInvalid(START_POINT | STARTING_AZIMUTH | END_POINT |
ENDING_AZIMUTH | GEODESIC_DISTANCE)) {
if (isInvalid(END_POINT)) {
@@ -647,8 +653,8 @@ public class GeodeticCalculator {
computeDistance();
}
}
- final PathBuilder bezier = new PathBuilder(toDegrees(tolerance /
radius));
- final Shape path;
+ final PathBuilder bezier = new PathBuilder(tolerance);
+ final Path2D path;
try {
path = bezier.build();
} finally {
@@ -658,17 +664,53 @@ public class GeodeticCalculator {
}
/**
+ * Creates an approximation of the region at a constant geodesic distance
around the start point.
+ * The returned shape is circlelike with the {@linkplain #getStartPoint()
start point} in its center.
+ * The coordinates are expressed in the coordinate reference system
specified at creation time.
+ * The approximation uses cubic Bézier curves.
+ *
+ * <p>This method tries to stay within the given tolerance threshold of
the geodesic track.
+ * The {@code tolerance} parameter should not be too small for avoiding
creation of unreasonably long chain of Bézier curves.
+ * For example a value of 1/10 of geodesic length may be sufficient.</p>
+ *
+ * <b>Limitations:</b>
+ * This method depends on the presence of {@code java.desktop} module.
This limitations may be addressed
+ * in a future Apache SIS version (see <a
href="https://issues.apache.org/jira/browse/SIS-453">SIS-453</a>).
+ *
+ * @param tolerance maximal error in the units of measurement given by
{@link #getDistanceUnit()}.
+ * This is approximate; the actual errors may vary
around that value.
+ * @return an approximation of circular region as a Java2D object.
+ * @throws TransformException if the coordinates can not be transformed to
{@linkplain #getPositionCRS() position CRS}.
+ * @throws IllegalStateException if some required properties have not been
specified.
+ */
+ public Shape createCircularRegion2D(final double tolerance) throws
TransformException {
+ ArgumentChecks.ensureStrictlyPositive("tolerance", tolerance);
+ if (isInvalid(START_POINT | GEODESIC_DISTANCE)) {
+ computeDistance();
+ }
+ final CircularPath bezier = new CircularPath(tolerance);
+ final Path2D path;
+ try {
+ path = bezier.build();
+ } finally {
+ bezier.reset();
+ }
+ path.closePath();
+ return path;
+ }
+
+ /**
* Builds a geodesic path as a sequence of Bézier curves. The start point
and end points are the points
* in enclosing {@link GeodeticCalculator} at the time this class is
instantiated. The start coordinates
* given by {@link #φ1} and {@link #λ1} shall never change for this whole
builder lifetime. However the
* end coordinates ({@link #φ2}, {@link #λ2}) will vary at each step.
*/
- private final class PathBuilder extends Bezier {
+ private class PathBuilder extends Bezier {
/**
- * End point coordinates and derivative, together with geodesic and
loxodromic distances.
+ * The initial (i) and final (f) coordinates and derivatives, together
with geodesic and loxodromic distances.
* Saved for later restoration by {@link #reset()}.
*/
- private final double φf, λf, αf, distance, length;
+ private final double αi, αf, φf, λf, distance, length;
/**
* {@link #validity} flags at the time {@code PathBuilder} is
instantiated.
@@ -682,17 +724,19 @@ public class GeodeticCalculator {
private final double tolerance;
/**
- * Creates a builder for the given tolerance at equator in degrees.
+ * Creates a builder for the given tolerance at equator in metres.
*/
- PathBuilder(final double tolerance) {
+ PathBuilder(final double εx) {
super(ReferencingUtilities.getDimension(userToGeodetic.defaultCRS));
- this.tolerance = tolerance;
- φf = φ2;
- λf = λ2;
- αf = α2;
- distance = geodesicDistance;
- length = rhumblineLength;
- flags = validity;
+ αi = α1;
+ αf = α2;
+ φf = φ2;
+ λf = λ2;
+ α1 = IEEEremainder(α1, 2*PI); // For reliable
signum(α1) result.
+ tolerance = toDegrees(εx / radius);
+ distance = geodesicDistance;
+ length = rhumblineLength;
+ flags = validity;
}
/**
@@ -717,11 +761,20 @@ public class GeodeticCalculator {
α2 = αf;
} else {
geodesicDistance = distance * t;
+ validity |= GEODESIC_DISTANCE;
computeEndPoint();
}
- final double sign = signum(α1);
- if (sign == signum(λ1 - λ2)) {
- λ2 += 2*PI * sign; // We need λ₁ < λ₂ if heading
east, or λ₁ > λ₂ if heading west.
+ return evaluateAtEndPoint();
+ }
+
+ /**
+ * Implementation of {@link #evaluateAt(double)} using the current φ₂,
λ₂ and α₂ values.
+ * This method stores the projected coordinates in the {@link #point}
array and returns
+ * the derivative ∂y/∂x.
+ */
+ final double evaluateAtEndPoint() throws TransformException {
+ if ((λ2 - λ1) * α1 < 0) {
+ λ2 += 2*PI * signum(α1); // We need λ₁ < λ₂ if
heading east, or λ₁ > λ₂ if heading west.
}
final Matrix d = geographic(φ2, λ2).inverseTransform(point); //
Coordinates and Jacobian of point.
final double m00 = d.getElement(0,0);
@@ -736,20 +789,54 @@ public class GeodeticCalculator {
* α2 is azimuth angle in radians, with 0 pointing toward north
and values increasing clockwise.
* d is the Jacobian matrix from (φ,λ) to the user coordinate
reference system.
*/
- final double dx = cos(α2); // sin(π/2 - α) = -sin(α -
π/2) = cos(α)
- final double dy = sin(α2); // cos(π/2 - α) = +cos(α -
π/2) = sin(α)
+ final double dx = cos(α2); // sin(π/2 - α) = -sin(α -
π/2) = cos(α)
+ final double dy = sin(α2); // cos(π/2 - α) = +cos(α -
π/2) = sin(α)
final double tx = m00*dx + m01*dy;
final double ty = m10*dx + m11*dy;
return ty / tx;
}
/**
+ * Returns whether the point at given (<var>x</var>, <var>y</var>)
coordinates is close to the geodesic path.
+ * This method is invoked when the {@link Bezier} helper class thinks
that the point is not on the path, but
+ * could be wrong because of the difficulty to evaluate the Bézier
<var>t</var> parameter of closest point.
+ */
+ @Override
+ protected boolean isValid(final double x, final double y) throws
TransformException {
+ /*
+ * Following code is equivalent to `setEndPoint(new
DirectPosition2D(x, y))` but without checks
+ * for argument validity and with less temporary objects creation
(we recycle the `point` array).
+ */
+ point[0] = x;
+ point[1] = y;
+ for (int i=2; i<point.length; i++) {
+ point[i] = 0;
+ }
+ userToGeodetic.transform(point);
+ φ2 = toRadians(point[0]);
+ λ2 = toRadians(point[1]);
+ validity |= END_POINT;
+ /*
+ * Computes the azimuth to the given point. This azimuth may be
different than the azimuth of the
+ * geodesic path we are building. Compute the point that we would
have if the azimuth was correct
+ * and check the distance between those two points.
+ */
+ computeDistance();
+ α1 = αi;
+ computeEndPoint();
+ final DirectPosition p = geographic(φ2, λ2).inverseTransform();
+ return abs(p.getOrdinate(0) - x) <= εx &&
+ abs(p.getOrdinate(1) - y) <= εy;
+ }
+
+ /**
* Restores the enclosing {@link GeodeticCalculator} to the state that
it has at {@code PathBuilder} instantiation time.
*/
- void reset() {
+ final void reset() {
+ α1 = αi;
+ α2 = αf;
φ2 = φf;
λ2 = λf;
- α2 = αf;
geodesicDistance = distance;
rhumblineLength = length;
validity = flags;
@@ -757,6 +844,48 @@ public class GeodeticCalculator {
}
/**
+ * Builds a circular region around the start point. The shape is created
as a sequence of Bézier curves.
+ */
+ private final class CircularPath extends PathBuilder {
+ /**
+ * Creates a builder for the given tolerance at equator in degrees.
+ */
+ CircularPath(final double εx) {
+ super(εx);
+ }
+
+ /**
+ * Invoked for computing a new point on the circular path. This method
is invoked with a <var>t</var> value varying from
+ * 0 to 1 inclusive. The <var>t</var> value is multiplied by 2π for
getting an angle. This method stores the coordinates
+ * in the {@link #point} array and returns the derivative (∂y/∂x) at
that location.
+ *
+ * @param t angle fraction from 0 to 1 inclusive.
+ * @return derivative (∂y/∂x) at the point.
+ * @throws TransformException if the point coordinates can not be
computed.
+ */
+ @Override
+ protected double evaluateAt(final double t) throws TransformException {
+ α1 = IEEEremainder((t - 0.5) * (2*PI), 2*PI);
+ validity |= STARTING_AZIMUTH;
+ computeEndPoint();
+ final double d = evaluateAtEndPoint();
+ if (depth <= 1) {
+ // Force division of the curve in two smaller curves. We want
at least 4 Bézier curves in an ellipse.
+ εx = εy = -1;
+ }
+ return d;
+ }
+
+ /**
+ * No additional test (compared to {@link Bezier} base class) for
determining if the point is close enough.
+ */
+ @Override
+ protected boolean isValid(final double x, final double y) throws
TransformException {
+ return false;
+ }
+ }
+
+ /**
* Returns a string representation of start point, end point, azimuths and
distance.
* The text representation is implementation-specific and may change in
any future version.
* Current implementation is like below:
diff --git
a/core/sis-referencing/src/test/java/org/apache/sis/referencing/GeodeticCalculatorTest.java
b/core/sis-referencing/src/test/java/org/apache/sis/referencing/GeodeticCalculatorTest.java
index 70751ec..c904168 100644
---
a/core/sis-referencing/src/test/java/org/apache/sis/referencing/GeodeticCalculatorTest.java
+++
b/core/sis-referencing/src/test/java/org/apache/sis/referencing/GeodeticCalculatorTest.java
@@ -31,6 +31,8 @@ import
org.apache.sis.internal.referencing.j2d.ShapeUtilitiesExt;
import org.apache.sis.internal.referencing.Formulas;
import org.apache.sis.geometry.DirectPosition2D;
import org.apache.sis.util.CharSequences;
+import org.apache.sis.math.StatisticsFormat;
+import org.apache.sis.math.Statistics;
import org.apache.sis.measure.Units;
import org.apache.sis.test.widget.VisualCheck;
import org.apache.sis.test.OptionalTestData;
@@ -213,9 +215,27 @@ public final strictfp class GeodeticCalculatorTest extends
TestCase {
}
/**
- * Tests {@link GeodeticCalculator#toGeodesicPath2D(double)}. This method
uses a CRS that swap axis order
- * as a way to verify that user-specified CRS is taken in account. The
start point and end point are the
- * same than in {@link #testGeodesicDistanceAndAzimuths()}. Note that this
path crosses the anti-meridian,
+ * Tests {@link GeodeticCalculator#createCircularRegion2D(double)}.
+ *
+ * @throws TransformException if an error occurred while transforming
coordinates.
+ */
+ @Test
+ @DependsOnMethod("testUsingTransform")
+ public void testCircularRegion2D() throws TransformException {
+ final GeodeticCalculator c = create(true);
+ c.setStartPoint(-33.0, -71.6); // Valparaíso
+ c.setGeodesicDistance(100000); // 100 km
+ Shape region = c.createCircularRegion2D(10000);
+ if (VisualCheck.SHOW_WIDGET) {
+ VisualCheck.show(region);
+ }
+ // TODO: test bounding box.
+ }
+
+ /**
+ * Tests {@link GeodeticCalculator#createGeodesicPath2D(double)}. This
method uses a CRS that swap axis order
+ * as a way to verify that user-specified CRS is taken in account. The
start point and end point are the same
+ * than in {@link #testGeodesicDistanceAndAzimuths()}. Note that this path
crosses the anti-meridian,
* so the end point needs to be shifted by 360°.
*
* @throws TransformException if an error occurred while transforming
coordinates.
@@ -225,10 +245,10 @@ public final strictfp class GeodeticCalculatorTest
extends TestCase {
public void testGeodesicPath2D() throws TransformException {
final GeodeticCalculator c = create(true);
final double tolerance = 0.05;
- c.setStartPoint(-33.0, -71.6);
// Valparaíso
- c.setEndPoint ( 31.4, 121.8);
// Shanghai
- final Shape singleCurve = c.toGeodesicPath2D(Double.POSITIVE_INFINITY);
- final Shape multiCurves = c.toGeodesicPath2D(10000);
// 10 km tolerance.
+ c.setStartPoint(-33.0, -71.6);
// Valparaíso
+ c.setEndPoint ( 31.4, 121.8);
// Shanghai
+ final Shape singleCurve =
c.createGeodesicPath2D(Double.POSITIVE_INFINITY);
+ final Shape multiCurves = c.createGeodesicPath2D(10000);
// 10 km tolerance.
/*
* The approximation done by a single curve is not very good, but is
easier to test.
*/
@@ -257,7 +277,7 @@ public final strictfp class GeodeticCalculatorTest extends
TestCase {
c.setEndPoint (0, 12);
assertEquals(-90, c.getStartingAzimuth(), tolerance);
assertEquals(-90, c.getEndingAzimuth(), tolerance);
- final Shape geodeticCurve = c.toGeodesicPath2D(1);
+ final Shape geodeticCurve = c.createGeodesicPath2D(1);
final double[] coords = new double[2];
for (final PathIterator it = geodeticCurve.getPathIterator(null, 1);
!it.isDone(); it.next()) {
it.currentSegment(coords);
@@ -277,6 +297,7 @@ public final strictfp class GeodeticCalculatorTest extends
TestCase {
final Random random = TestUtilities.createRandomNumberGenerator();
final GeodeticCalculator c = create(false);
final Geodesic reference = new
Geodesic(c.ellipsoid.getSemiMajorAxis(), 1/c.ellipsoid.getInverseFlattening());
+ final StatisticsFormat sf = VERBOSE ? StatisticsFormat.getInstance() :
null;
for (int i=0; i<100; i++) {
final double φ1 = random.nextDouble() * 180 - 90;
final double λ1 = random.nextDouble() * 360 - 180;
@@ -292,38 +313,53 @@ public final strictfp class GeodeticCalculatorTest
extends TestCase {
assertEquals("Starting azimuth", expected.azi1,
c.getStartingAzimuth(), Formulas.ANGULAR_TOLERANCE);
assertEquals("Ending azimuth", expected.azi2,
c.getEndingAzimuth(), Formulas.ANGULAR_TOLERANCE);
assertTrue ("Rhumb ≧ geodesic", rhumbLine >= geodesic);
- if (false) {
- // Disabled because currently too inaccurate - see
https://issues.apache.org/jira/browse/SIS-453
- assertEquals("Distance measured along geodesic path",
geodesic, length(c), Formulas.ANGULAR_TOLERANCE);
+ if (sf != null) {
+ // Checks the geodesic path on only 10% of test data, because
this computation is expensive.
+ if ((i % 10) == 0) {
+ out.println(c);
+ out.println(sf.format(geodesicPathFitness(c, 1000)));
+ }
}
}
}
/**
- * Measures an estimation of the length of the path returned by {@link
GeodeticCalculator#toGeodesicPath2D(double)}.
- * This method iterates over line segments and use the given calculator
for computing the geodesic distance of each
- * segment. The state of the given calculator is modified by this method.
+ * Estimates the differences between the points on the Bézier curves and
the points computed by geodetic calculator.
+ * This method estimates the length of the path returned by {@link
GeodeticCalculator#createGeodesicPath2D(double)}
+ * and compares with the expected distance and azimuth at each point, by
iterating over line segments and computing
+ * the geodesic distance of each segment. The state of the given
calculator is modified by this method.
+ *
+ * @param resolution tolerance threshold for the curve approximation, in
metres.
+ * @return statistics about errors relative to the resolution.
*/
- private static double length(final GeodeticCalculator c) throws
TransformException {
- final PathIterator iterator =
c.toGeodesicPath2D(10).getPathIterator(null, 100);
- final double[] buffer = new double[2];
- double length=0;
+ private static Statistics[] geodesicPathFitness(final GeodeticCalculator
c, final double resolution) throws TransformException {
+ final PathIterator iterator =
c.createGeodesicPath2D(resolution).getPathIterator(null,
Formulas.ANGULAR_TOLERANCE);
+ final Statistics xError = new Statistics("Δx/r");
+ final Statistics yError = new Statistics("Δy/r");
+ final Statistics aErrors = new Statistics("Δα (°)");
+ final double azimuth = c.getStartingAzimuth();
+ final double toMetres = (PI/180) *
Formulas.getAuthalicRadius(c.ellipsoid);
+ final double[] buffer = new double[2];
while (!iterator.isDone()) {
switch (iterator.currentSegment(buffer)) {
- default: fail("Unexpected path"); break;
- case PathIterator.SEG_MOVETO: {
- c.setStartPoint(buffer[0], buffer[1]);
- break;
- }
+ default: fail("Unexpected segment"); break;
+ case PathIterator.SEG_MOVETO: break;
case PathIterator.SEG_LINETO: {
c.setEndPoint(buffer[0], buffer[1]);
- length += c.getGeodesicDistance();
- c.moveToEndPoint();
+ aErrors.accept(abs(c.getStartingAzimuth() - azimuth));
+ c.setStartingAzimuth(azimuth);
+ DirectPosition endPoint = c.getEndPoint();
+ final double φ = endPoint.getOrdinate(0);
+ final double λ = endPoint.getOrdinate(1);
+ double dy = (buffer[0] - φ) * toMetres;
+ double dx = IEEEremainder(buffer[1] - λ, 360) * toMetres *
cos(toRadians(φ));
+ yError.accept(abs(dy) / resolution);
+ xError.accept(abs(dx) / resolution);
}
}
iterator.next();
}
- return length;
+ return new Statistics[] {xError, yError, aErrors};
}
/**
diff --git a/ide-project/NetBeans/nbproject/genfiles.properties
b/ide-project/NetBeans/nbproject/genfiles.properties
index b11d782..89d141d 100644
--- a/ide-project/NetBeans/nbproject/genfiles.properties
+++ b/ide-project/NetBeans/nbproject/genfiles.properties
@@ -3,6 +3,6 @@
build.xml.data.CRC32=58e6b21c
build.xml.script.CRC32=462eaba0
[email protected]
-nbproject/build-impl.xml.data.CRC32=790389f0
+nbproject/build-impl.xml.data.CRC32=15917a2f
nbproject/build-impl.xml.script.CRC32=a7689f96
nbproject/[email protected]
diff --git a/ide-project/NetBeans/nbproject/project.xml
b/ide-project/NetBeans/nbproject/project.xml
index 233ef9b..c22b81f 100644
--- a/ide-project/NetBeans/nbproject/project.xml
+++ b/ide-project/NetBeans/nbproject/project.xml
@@ -86,6 +86,7 @@
<word>boolean</word>
<word>Bézier</word>
<word>centimetre</word>
+ <word>circlelike</word>
<word>classname</word>
<word>classnames</word>
<word>classpath</word>