diff --git a/modules/app/src/main/java/org/locationtech/jtstest/function/DistanceFunctions.java b/modules/app/src/main/java/org/locationtech/jtstest/function/DistanceFunctions.java
index 649db20317..e26b35335a 100644
--- a/modules/app/src/main/java/org/locationtech/jtstest/function/DistanceFunctions.java
+++ b/modules/app/src/main/java/org/locationtech/jtstest/function/DistanceFunctions.java
@@ -13,6 +13,7 @@
import org.locationtech.jts.algorithm.distance.DiscreteFrechetDistance;
import org.locationtech.jts.algorithm.distance.DiscreteHausdorffDistance;
+import org.locationtech.jts.algorithm.distance.DirectedHausdorffDistance;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.LineString;
@@ -45,53 +46,61 @@ public static Geometry frechetDistanceLine(Geometry a, Geometry b)
return a.getFactory().createLineString(dist.getCoordinates());
}
- public static double hausdorffDistance(Geometry a, Geometry b)
+ @Metadata(description="Oriented discrete Hausdorff distance from A to B")
+ public static double orientedDiscreteHausdorffDistance(Geometry a, Geometry b)
+ {
+ return DiscreteHausdorffDistance.orientedDistance(a, b);
+ }
+
+ @Metadata(description="Oriented discrete Hausdorff distance line from A to B, densified")
+ public static Geometry orientedDiscreteHausdorffLineDensify(Geometry a, Geometry b,
+ @Metadata(title="Densify fraction")
+ double frac)
{
- return DiscreteHausdorffDistance.distance(a, b);
+ return DiscreteHausdorffDistance.orientedDistanceLine(a, b, frac);
+ }
+
+ @Metadata(description="Clipped directed Hausdorff distance from A to B")
+ public static Geometry clippedDirectedHausdorffLine(Geometry a, Geometry b)
+ {
+ Geometry clippedLine = LinearReferencingFunctions.project(a, b);
+ Coordinate[] pts = DirectedHausdorffDistance.distancePoints(clippedLine, b);
+ return a.getFactory().createLineString(pts);
}
- @Metadata(description="Hausdorff distance between A and B")
- public static Geometry hausdorffDistanceLine(Geometry a, Geometry b)
+ @Metadata(description="Directed Hausdorff distance from A to B, up to tolerance")
+ public static double directedHausdorffDistance(Geometry a, Geometry b,
+ @Metadata(title="Distance tolerance")
+ double distTol)
{
- return DiscreteHausdorffDistance.distanceLine(a, b);
+ return DirectedHausdorffDistance.distance(a, b, distTol);
}
-
- @Metadata(description="Hausdorff distance between A and B, densified")
- public static Geometry hausdorffDistanceLineDensify(Geometry a, Geometry b,
- @Metadata(title="Densify fraction")
- double frac)
- {
- return DiscreteHausdorffDistance.distanceLine(a, b, frac);
- }
-
- @Metadata(description="Oriented Hausdorff distance from A to B")
- public static Geometry orientedHausdorffDistanceLine(Geometry a, Geometry b)
+
+ @Metadata(description="Directed Hausdorff distance line from A to B, up to tolerance")
+ public static Geometry directedHausdorffLineTol(Geometry a, Geometry b,
+ @Metadata(title="Distance tolerance")
+ double distTol)
{
- return DiscreteHausdorffDistance.orientedDistanceLine(a, b);
+ Coordinate[] pts = DirectedHausdorffDistance.distancePoints(a, b, distTol);
+ return a.getFactory().createLineString(pts);
}
-
- @Metadata(description="Oriented Hausdorff distance from A to B")
- public static Geometry clippedOrientedHausdorffDistanceLine(Geometry a, Geometry b)
+
+ @Metadata(description="Directed Hausdorff distance line from A to B")
+ public static Geometry directedHausdorffLine(Geometry a, Geometry b)
{
- //TODO: would this be more efficient done as part of DiscreteHausdorffDistance?
- Geometry clippedLine = LinearReferencingFunctions.project(a, b);
- return DiscreteHausdorffDistance.orientedDistanceLine(clippedLine, b);
+ Coordinate[] pts = DirectedHausdorffDistance.distancePoints(a, b);
+ return a.getFactory().createLineString(pts);
}
-
- @Metadata(description="Oriented Hausdorff distance from A to B")
- public static double orientedHausdorffDistance(Geometry a, Geometry b)
- {
- return DiscreteHausdorffDistance.orientedDistance(a, b);
- }
-
- @Metadata(description="Oriented Hausdorff distance from A to B, densified")
- public static Geometry orientedHausdorffDistanceLineDensify(Geometry a, Geometry b,
- @Metadata(title="Densify fraction")
- double frac)
+
+ @Metadata(description="Hausdorff distance between A and B, up to tolerance")
+ public static Geometry hausdorffLine(Geometry a, Geometry b)
{
- return DiscreteHausdorffDistance.orientedDistanceLine(a, b, frac);
+ Coordinate[] pts = DirectedHausdorffDistance.hausdorffDistancePoints(a, b);
+ return a.getFactory().createLineString(pts);
}
-
+
+ //--------------------------------------------
+
public static double distanceIndexed(Geometry a, Geometry b) {
return IndexedFacetDistance.distance(a, b);
}
@@ -117,4 +126,5 @@ public static Geometry nearestPointsIndexedEachB(Geometry a, Geometry b) {
return a.getFactory().createMultiLineString(lines);
}
+
}
diff --git a/modules/app/src/main/java/org/locationtech/jtstest/function/SelectionFunctions.java b/modules/app/src/main/java/org/locationtech/jtstest/function/SelectionFunctions.java
index e3c01b6b19..799544c807 100644
--- a/modules/app/src/main/java/org/locationtech/jtstest/function/SelectionFunctions.java
+++ b/modules/app/src/main/java/org/locationtech/jtstest/function/SelectionFunctions.java
@@ -16,6 +16,7 @@
import java.util.List;
import org.locationtech.jts.algorithm.construct.MaximumInscribedCircle;
+import org.locationtech.jts.algorithm.distance.DirectedHausdorffDistance;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.prep.PreparedGeometry;
import org.locationtech.jts.geom.prep.PreparedGeometryFactory;
@@ -238,6 +239,25 @@ public boolean isTrue(Geometry g) {
});
}
+ public static Geometry fullyWithinDistance(Geometry a, final Geometry mask, double maximumDistance)
+ {
+ return select(a, new GeometryPredicate() {
+ public boolean isTrue(Geometry g) {
+ return DirectedHausdorffDistance.isFullyWithinDistance(g, mask, maximumDistance);
+ }
+ });
+ }
+
+ public static Geometry fullyWithinDistancePrep(Geometry a, final Geometry mask, double maximumDistance)
+ {
+ DirectedHausdorffDistance dhd = new DirectedHausdorffDistance(mask);
+ return select(a, new GeometryPredicate() {
+ public boolean isTrue(Geometry g) {
+ return dhd.isFullyWithinDistance(g, maximumDistance);
+ }
+ });
+ }
+
public static Geometry maxInCircleRadiusWithin(Geometry a,
@Metadata(title="Max Radius Length")
double maximumRadius)
diff --git a/modules/core/src/main/java/org/locationtech/jts/algorithm/construct/IndexedDistanceToPoint.java b/modules/core/src/main/java/org/locationtech/jts/algorithm/construct/IndexedDistanceToPoint.java
index cac5810db9..9ba6f208dc 100644
--- a/modules/core/src/main/java/org/locationtech/jts/algorithm/construct/IndexedDistanceToPoint.java
+++ b/modules/core/src/main/java/org/locationtech/jts/algorithm/construct/IndexedDistanceToPoint.java
@@ -11,6 +11,7 @@
*/
package org.locationtech.jts.algorithm.construct;
+import org.locationtech.jts.algorithm.locate.IndexedPointInPolygonsLocator;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.Location;
diff --git a/modules/core/src/main/java/org/locationtech/jts/algorithm/distance/DirectedHausdorffDistance.java b/modules/core/src/main/java/org/locationtech/jts/algorithm/distance/DirectedHausdorffDistance.java
new file mode 100644
index 0000000000..285a077fa6
--- /dev/null
+++ b/modules/core/src/main/java/org/locationtech/jts/algorithm/distance/DirectedHausdorffDistance.java
@@ -0,0 +1,844 @@
+/*
+ * Copyright (c) 2026 Martin Davis
+ *
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License 2.0
+ * and Eclipse Distribution License v. 1.0 which accompanies this distribution.
+ * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html
+ * and the Eclipse Distribution License is available at
+ *
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ */
+package org.locationtech.jts.algorithm.distance;
+
+import java.util.Iterator;
+import java.util.PriorityQueue;
+
+import org.locationtech.jts.algorithm.construct.LargestEmptyCircle;
+import org.locationtech.jts.algorithm.locate.IndexedPointInPolygonsLocator;
+import org.locationtech.jts.geom.Coordinate;
+import org.locationtech.jts.geom.Dimension;
+import org.locationtech.jts.geom.Envelope;
+import org.locationtech.jts.geom.Geometry;
+import org.locationtech.jts.geom.GeometryCollectionIterator;
+import org.locationtech.jts.geom.GeometryComponentFilter;
+import org.locationtech.jts.geom.LineString;
+import org.locationtech.jts.geom.Location;
+import org.locationtech.jts.geom.Point;
+import org.locationtech.jts.io.WKTWriter;
+import org.locationtech.jts.operation.distance.CoordinateSequenceLocation;
+import org.locationtech.jts.operation.distance.IndexedFacetDistance;
+
+/**
+ * Computes the directed Hausdorff distance from one geometry to another.
+ * The directed Hausdorff distance is the maximum distance any point
+ * on a query geometry A can be from a target geometry B.
+ * Equivalently, every point in the query geometry is within that distance
+ * of the target geometry.
+ * The class can compute a pair of points at which the distance is attained:
+ * [ farthest A point, nearest B point ].
+ *
+ *The directed Hausdorff distance (DHD) is defined as:
+ *
+ * DHD(A,B) = maxa ∈ A (maxb ∈ B (distance(a, b) )
+ *
+ *
+ * DHD is asymmetric: DHD(A,B) may not be equal to DHD(B,A).
+ * Hence it is not a distance metric.
+ * The Hausdorff distance is is a symmetric distance metric defined as:
+ *
+ * HD(A,B) = max(DHD(A,B), DHD(B,A))
+ *
+ * This can be computed via the
+ * {@link #hausdorffDistancePoints(Geometry, Geometry)} function.
+ *
+ * Points, lines and polygons are supported as input.
+ * If the query geometry is polygonal,
+ * the point at maximum distance may occur in the interior of a query polygon.
+ * For a polygonal target geometry the point always lies on the boundary.
+ *
+ * The directed Hausdorff distance can be used to test
+ * whether a geometry lies fully within a given distance of another one.
+ * The {@link #isFullyWithinDistance(Geometry, double)} function
+ * is provided to execute this test efficiently.
+ * It implements heuristic checks and short-circuiting to improve performance.
+ *
+ * The class can be used in prepared mode.
+ * Creating an instance on a target geometry caches indexes for that geometry.
+ * Then {@link #farthestPoints(Geometry)
+ * or {@link #isFullyWithinDistance(Geometry, double)}
+ * can be called efficiently for multiple query geometries.
+ *
+ * If the Hausdorff distance is attained at a non-vertex of the query geometry,
+ * the location must be approximated.
+ * The algorithm uses a distance tolerance to control the approximation accuracy.
+ * The tolerance is automatically determined to balance between accuracy and performance.
+ * if more accuracy is desired some function signatures are provided
+ * which allow specifying a distance tolerance .
+ *
+ * This algorithm is easier to use, more accurate,
+ * and much faster than {@link DiscreteHausdorffDistance}.
+ *
+ * @author Martin Davis
+ *
+ */
+public class DirectedHausdorffDistance {
+
+ /**
+ * Computes the directed Hausdorff distance
+ * of a query geometry A from a target one B.
+ *
+ * @param a the query geometry
+ * @param b the target geometry
+ * @return the directed Hausdorff distance,
+ * or NaN if an input is empty
+ */
+ public static double distance(Geometry a, Geometry b)
+ {
+ DirectedHausdorffDistance hd = new DirectedHausdorffDistance(b);
+ return distance(hd.farthestPoints(a));
+ }
+
+ /**
+ * Computes the directed Hausdorff distance
+ * of a query geometry A from a target one B,
+ * up to a given distance accuracy.
+ *
+ * @param a the query geometry
+ * @param b the target geometry
+ * @param tolerance the accuracy distance tolerance
+ * @return the directed Hausdorff distance,
+ * or NaN if an input is empty
+ */
+ public static double distance(Geometry a, Geometry b, double tolerance)
+ {
+ DirectedHausdorffDistance hd = new DirectedHausdorffDistance(b);
+ return distance(hd.farthestPoints(a, tolerance));
+ }
+
+ /**
+ * Computes a line containing a pair of points which attain the directed Hausdorff distance
+ * of a query geometry A from a target one B.
+ *
+ * @param a the query geometry
+ * @param b the target geometry
+ * @param tolerance the accuracy distance tolerance
+ * @return a pair of points [ptA, ptB] demonstrating the distance,
+ * or null if an input is empty
+ */
+ public static Coordinate[] distancePoints(Geometry a, Geometry b)
+ {
+ DirectedHausdorffDistance dhd = new DirectedHausdorffDistance(b);
+ return dhd.farthestPoints(a);
+ }
+
+ /**
+ * Computes a line containing a pair of points which attain the directed Hausdorff distance
+ * of a query geometry A from a target one B, up to a given distance accuracy.
+ *
+ * @param a the query geometry
+ * @param b the target geometry
+ * @param tolerance the accuracy distance tolerance
+ * @return a pair of points [ptA, ptB] demonstrating the distance,
+ * or null if an input is empty
+ */
+ public static Coordinate[] distancePoints(Geometry a, Geometry b, double tolerance)
+ {
+ DirectedHausdorffDistance dhd = new DirectedHausdorffDistance(b);
+ return dhd.farthestPoints(a, tolerance);
+ }
+
+ /**
+ * Computes a pair of points which attain the symmetric Hausdorff distance
+ * between two geometries.
+ * This the maximum of the two directed Hausdorff distances.
+ *
+ * @param a a geometry
+ * @param b a geometry
+ * @return a pair of points [ptA, ptB] demonstrating the Hausdorff distance,
+ * or null if an input is empty
+ */
+ public static Coordinate[] hausdorffDistancePoints(Geometry a, Geometry b)
+ {
+ DirectedHausdorffDistance hdAB = new DirectedHausdorffDistance(b);
+ Coordinate[] ptsAB = hdAB.farthestPoints(a);
+ DirectedHausdorffDistance hdBA = new DirectedHausdorffDistance(a);
+ Coordinate[] ptsBA = hdBA.farthestPoints(b);
+
+ //-- return points in A-B order
+ Coordinate[] pts = ptsAB;
+ if (distance(ptsBA) > distance(ptsAB)) {
+ //-- reverse the BA points
+ pts = pair(ptsBA[1], ptsBA[0]);
+ }
+ return pts;
+ }
+
+ /**
+ * Computes the symmetric Hausdorff distance between two geometries.
+ * This the maximum of the two directed Hausdorff distances.
+ *
+ * @param a a geometry
+ * @param b a geometry
+ * @return the Hausdorff distance, or NaN if an input is empty
+ */
+ public static double hausdorffDistance(Geometry a, Geometry b)
+ {
+ return distance(hausdorffDistancePoints(a, b));
+ }
+
+ /**
+ * Computes whether a query geometry lies fully within a give distance of a target geometry.
+ * Equivalently, detects whether any point of the query geometry is farther
+ * from the target than the specified distance.
+ * This is the case if DHD(A, B) > maxDistance.
+ *
+ * @param a the query geometry
+ * @param b the target geometry
+ * @param maxDistance the distance limit
+ * @return true if the query geometry lies fully within the distance of the target
+ */
+ public static boolean isFullyWithinDistance(Geometry a, Geometry b, double maxDistance)
+ {
+ DirectedHausdorffDistance hd = new DirectedHausdorffDistance(b);
+ return hd.isFullyWithinDistance(a, maxDistance);
+ }
+
+ /**
+ * Computes whether a query geometry lies fully within a give distance of a target geometry,
+ * up to a given distance accuracy.
+ * Equivalently, detects whether any point of the query geometry is farther
+ * from the target than the specified distance.
+ * This is the case if DHD(A, B) > maxDistance.
+ *
+ * @param a the query geometry
+ * @param b the target geometry
+ * @param maxDistance the distance limit
+ * @param tolerance the accuracy distance tolerance
+ * @return true if the query geometry lies fully within the distance of the target
+ */
+ public static boolean isFullyWithinDistance(Geometry a, Geometry b, double maxDistance, double tolerance)
+ {
+ DirectedHausdorffDistance hd = new DirectedHausdorffDistance(b);
+ return hd.isFullyWithinDistance(a, maxDistance, tolerance);
+ }
+
+ private static double distance(Coordinate[] pts) {
+ if (pts == null)
+ return EMPTY_DISTANCE;
+ return pts[0].distance(pts[1]);
+ }
+
+ private static Coordinate[] pair(Coordinate p0, Coordinate p1) {
+ return new Coordinate[] { p0.copy(), p1.copy() };
+ }
+
+ private static final double EMPTY_DISTANCE = Double.NaN;
+
+ /**
+ * Heuristic automatic tolerance factor
+ */
+ private static final double AUTO_TOLERANCE_FACTOR = 1.0e4;
+
+ /**
+ * Heuristic factor to improve performance of area interior farthest point computation.
+ * The LargestEmptyCircle computation is much slower than the boundary one,
+ * is unlikely to occur, and accuracy is less critical (and obvious).
+ * Improve performance by using a coarser distance tolerance.
+ */
+ private static final double AREA_INTERIOR_TOLERANCE_FACTOR = 20;
+
+ /**
+ * Tolerance factor for isFullyWithinDistance.
+ * A larger factor is used to increase accuracy.
+ * The operation will usually short-circuit, so performance impact is low.
+ */
+ private static final double FULLY_WITHIN_TOLERANCE_FACTOR = 10 * AUTO_TOLERANCE_FACTOR;
+
+ /**
+ * Computes an automatic distance tolerance for a query geometry.
+ * A tolerance of zero may be returned (e.g. for a zero-length line);
+ * this is supported by the distance algorithm.
+ *
+ * @param geom the query geometry
+ * @return the distance tolerance
+ */
+ private static double computeTolerance(Geometry geom) {
+ return geom.getEnvelopeInternal().getDiameter() / AUTO_TOLERANCE_FACTOR;
+ }
+
+ private Geometry target;
+ private TargetDistance targetDistance;
+
+ /**
+ * Create a new instance for a target geometry.
+ *
+ * @param geom the geometry to compute the distance from
+ */
+ public DirectedHausdorffDistance(Geometry geom) {
+ this.target = geom;
+ targetDistance = new TargetDistance(this.target);
+ }
+
+ /**
+ * Tests whether a query geometry lies fully within a give distance of the target geometry.
+ * Equivalently, detects whether any point of the query geometry is farther
+ * from the target than the specified distance.
+ * This is the case if DHD(A, B) > maxDistance.
+ *
+ * @param geom the query geometry
+ * @param maxDistance the distance limit
+ * @return true if the query geometry lies fully within the distance of the target
+ */
+ public boolean isFullyWithinDistance(Geometry geom, double maxDistance) {
+ double tolerance = maxDistance / FULLY_WITHIN_TOLERANCE_FACTOR;
+ return isFullyWithinDistance(geom, maxDistance, tolerance);
+ }
+
+ /**
+ * Tests whether a query geometry lies fully within a give distance of the target geometry,
+ * up to a given distance accuracy.
+ * Equivalently, detects whether any point of the query geometry is farther
+ * from the target than the specified distance.
+ * This is the case if DHD(A, B) > maxDistance.
+ *
+ * @param geom the query geometry
+ * @param maxDistance the distance limit
+ * @param tolerance the accuracy distance tolerance
+ * @return true if the query geometry lies fully within the distance of the target
+ */
+ public boolean isFullyWithinDistance(Geometry geom, double maxDistance, double tolerance) {
+ //-- envelope checks
+ if (isBeyond(geom.getEnvelopeInternal(), target.getEnvelopeInternal(), maxDistance))
+ return false;
+
+ Coordinate[] maxDistCoords = computeDistancePoints(geom, tolerance, maxDistance);
+ //-- handle empty case
+ if (maxDistCoords == null)
+ return false;
+ return distance(maxDistCoords) <= maxDistance;
+ }
+
+ /**
+ * Tests if an envelope must have a side farther than the maximum distance
+ * from another envelope.
+ *
+ * @param envA
+ * @param envB
+ * @param maxDistance
+ * @return true if envelope A must have a far point from envelope B
+ */
+ private static boolean isBeyond(Envelope envA, Envelope envB, double maxDistance) {
+ /**
+ * At least one point of the geometry lies on each edge of the envelope,
+ * so if any edge is farther than maxDistance from the closest edge of the B envelope
+ * there must be a point at that distance (or further).
+ */
+ return envA.getMinX() < envB.getMinX() - maxDistance
+ || envA.getMinY() < envB.getMinY() - maxDistance
+ || envA.getMaxX() > envB.getMaxX() + maxDistance
+ || envA.getMaxY() > envB.getMaxY() + maxDistance;
+ }
+
+ /**
+ * Computes a pair of points which attain the directed Hausdorff distance
+ * of a query geometry A from the target B.
+ * If either geometry is empty the result is null.
+ *
+ * @param geom the query geometry
+ * @return a pair of points [ptA, ptB] attaining the distance,
+ * or null if an input is empty
+ */
+ public Coordinate[] farthestPoints(Geometry geom) {
+ double tolerance = computeTolerance(geom);
+ return farthestPoints(geom, tolerance);
+ }
+
+ /**
+ * Computes a pair of points which attain the directed Hausdorff distance
+ * of a query geometry A from the target B,
+ * up to a given distance accuracy.
+ * If either geometry is empty the result is null.
+ *
+ * @param geom the query geometry
+ * @param tolerance the approximation distance tolerance
+ * @return a pair of points [ptA, ptB] attaining the distance,
+ * or null if an input is empty
+ */
+ public Coordinate[] farthestPoints(Geometry geom, double tolerance) {
+ return computeDistancePoints(geom, tolerance, -1.0);
+ }
+
+ private Coordinate[] computeDistancePoints(Geometry geom, double tolerance, double maxDistanceLimit) {
+ /**
+ * Negative tolerances are not allowed.
+ * Zero tolerance is allowed, to support zero-size input.
+ */
+ if (tolerance < 0.0 ) {
+ throw new IllegalArgumentException("Tolerance must be non-negative");
+ }
+
+ if (geom.isEmpty() || target.isEmpty())
+ return null;
+
+ if (geom.getDimension() == Dimension.P) {
+ return computeForPoints(geom, maxDistanceLimit);
+ }
+ //TODO: handle mixed geoms with points
+ Coordinate[] maxDistPtsEdge = computeForEdges(geom, tolerance, maxDistanceLimit);
+
+ if (isBeyondLimit(distance(maxDistPtsEdge), maxDistanceLimit)) {
+ return maxDistPtsEdge;
+ }
+
+ /**
+ * Polygonal query geometry may have an interior point as the farthest point.
+ */
+ if (geom.getDimension() == Dimension.A) {
+ Coordinate[] maxDistPtsInterior = computeForAreaInterior(geom, tolerance);
+ if (maxDistPtsInterior != null
+ && distance(maxDistPtsInterior) > distance(maxDistPtsEdge)) {
+ return maxDistPtsInterior;
+ }
+ }
+ return maxDistPtsEdge;
+ }
+
+ private Coordinate[] computeForPoints(Geometry geom, double maxDistanceLimit) {
+ double maxDist = -1.0;;
+ Coordinate[] maxDistPtsAB = null;
+ Iterator geomi = new GeometryCollectionIterator(geom);
+ while (geomi.hasNext()) {
+ Geometry geomElem = (Geometry) geomi.next();
+ if (! (geomElem instanceof Point))
+ continue;
+
+ Coordinate pA = geomElem.getCoordinate();
+ Coordinate pB = targetDistance.nearestPoint(pA);
+ double dist = pA.distance(pB);
+
+ boolean isInterior = dist > 0 && targetDistance.isInterior(pA);
+ //-- check for interior point
+ if (isInterior) {
+ dist = 0;
+ pB = pA;
+ }
+ if (dist > maxDist) {
+ maxDist = dist;
+ maxDistPtsAB = pair(pA, pB);
+ }
+ if (isValidLimit(maxDistanceLimit)
+ && isBeyondLimit(maxDist, maxDistanceLimit)) {
+ break;
+ }
+ }
+ return maxDistPtsAB;
+ }
+
+ private Coordinate[] computeForEdges(Geometry geom, double tolerance, double maxDistanceLimit) {
+ PriorityQueue segQueue = createSegQueue(geom);
+
+ DHDSegment segMaxDist = null;
+ long iter = 0;
+ while (! segQueue.isEmpty()) {
+ iter++;
+ // get the segment with greatest distance bound
+ DHDSegment segMaxBound = segQueue.remove();
+ //System.out.println(segMaxBound);
+ //System.out.println(WKTWriter.toLineString(segMaxBound.getMaxDistPts()));
+
+/*
+ double maxDistBound = segMaxBound.getMaxDistanceBound();
+ double maxDist = segMaxBound.getMaxDistance();
+ System.out.format("%s len: %f bound: %f maxDist: %f\n",
+ segMaxBound, segMaxBound.getLength(), maxDistBound, maxDist);
+ if (maxDist > 1) {
+ System.out.println("FOUND");
+ }
+//*/
+ /**
+ * Save if segment point is farther than current farthest
+ */
+ if (segMaxDist == null
+ || segMaxBound.getMaxDistance() > segMaxDist.getMaxDistance()) {
+ segMaxDist = segMaxBound;
+ //System.out.println(">>>> " + WKTWriter.toLineString(segMaxDist.getMaxDistPts()));
+ }
+ /**
+ * Stop searching if remaining items in queue must all be closer
+ * than the current maximum distance.
+ */
+ if (segMaxBound.getMaxDistanceBound() <= segMaxDist.getMaxDistance()) {
+ break;
+ }
+ /**
+ * If maxDistanceLimit is specified, can stop searching if:
+ * - if segment distance bound is less than distance limit, no other segment can be farther
+ * - if a point of segment is farther than limit, isFulyWithin must be false
+ */
+ if (isValidLimit(maxDistanceLimit)) {
+ if (isWithinLimit(segMaxBound.getMaxDistanceBound(), maxDistanceLimit)
+ || isBeyondLimit(segMaxBound.getMaxDistance(), maxDistanceLimit)
+ ) {
+ break;
+ }
+ }
+
+ /**
+ * Check for equal or collinear segments.
+ * If so, don't bisect the segment further.
+ * This greatly improves performance when the inputs
+ * have identical or collinear segments
+ * (in particular, the case when the inputs are identical).
+ */
+ if (segMaxBound.getMaxDistance() == 0.0) {
+ if (isSameOrCollinear(segMaxBound))
+ continue;
+ }
+
+ //System.out.println(segMaxDist);
+
+ /**
+ * If segment is longer than tolerance
+ * it might provide a better max distance point,
+ * so bisect and keep searching.
+ */
+ if (tolerance > 0
+ && segMaxBound.getLength() > tolerance) {
+ DHDSegment[] bisects = segMaxBound.bisect(targetDistance);
+ addNonInterior(bisects[0], segQueue);
+ addNonInterior(bisects[1], segQueue);
+ }
+ }
+ /**
+ * A segment at maximum distance was found.
+ * Return the farthest point pair
+ */
+ if (segMaxDist != null)
+ return segMaxDist.getMaxDistPts();
+
+ /**
+ * No DHD segment was found.
+ * This must be because all were inside the target.
+ * In this case distance is zero.
+ * Return a single coordinate of the input as a representative point
+ */
+ Coordinate maxPt = geom.getCoordinate();
+ return pair(maxPt, maxPt);
+ }
+
+ private boolean isSameOrCollinear(DHDSegment seg) {
+ CoordinateSequenceLocation f0 = targetDistance.nearestLocation(seg.p0);
+ CoordinateSequenceLocation f1 = targetDistance.nearestLocation(seg.p1);
+ return f0.isSameSegment(f1);
+ }
+
+ private static boolean isValidLimit(double limit) {
+ return limit >= 0.0;
+ }
+
+ private static boolean isBeyondLimit(double maxDist, double maxDistanceLimit) {
+ return maxDistanceLimit >= 0 && maxDist > maxDistanceLimit;
+ }
+
+ private static boolean isWithinLimit(double maxDist, double maxDistanceLimit) {
+ return maxDistanceLimit >= 0 && maxDist <= maxDistanceLimit;
+ }
+
+ private void addNonInterior(DHDSegment segment, PriorityQueue segQueue) {
+ //-- discard segment if it is interior to a polygon
+ if (isInterior(segment)) {
+ return;
+ }
+ segQueue.add(segment);
+ }
+
+ /**
+ * Tests if segment is fully in the interior of the target geometry polygons (if any).
+ *
+ * @param segment
+ * @return
+ */
+ private boolean isInterior(DHDSegment segment) {
+ if (segment.getMaxDistance() > 0.0) {
+ return false;
+ }
+ return targetDistance.isInterior(segment.getEndpoint(0), segment.getEndpoint(1));
+ }
+
+ /**
+ * If the query geometry is polygonal, it is possible
+ * the farthest point lies in its interior.
+ * In this case it occurs at the centre of the Largest Empty Circle
+ * with B as obstacles and query geometry as constraint.
+ *
+ * This is a potentially expensive computation,
+ * especially for small tolerances.
+ * It is avoided where possible if heuristic checks
+ * can determine that the max distance is at
+ * the previously computed edge points.
+ *
+ * @param geom
+ * @param tolerance
+ * @return the maximum distance point pair at an interior point of a polygon,
+ * or null if it is known to not occur at an interior point
+ */
+ private Coordinate[] computeForAreaInterior(Geometry geom, double tolerance) {
+ if (tolerance <= 0.0)
+ return null;
+
+ //TODO: extract polygonal geoms from A
+ Geometry polygonal = geom;
+
+ /**
+ * Optimization - skip if A interior cannot intersect B,
+ * and thus farther point must lie on A boundary
+ */
+ if (polygonal.getEnvelopeInternal().disjoint(target.getEnvelopeInternal())) {
+ return null;
+ }
+
+ /**
+ * The LargestEmptyCircle computation is much slower than the boundary one,
+ * is quite unlikely to occur,
+ * and accuracy is probably less critical (or obvious).
+ * So improve performance by using a coarser distance tolerance.
+ */
+ //TODO: add short-circuiting based on maxDistanceLimit?
+
+ Point centerPt = LargestEmptyCircle.getCenter(target, polygonal, tolerance * AREA_INTERIOR_TOLERANCE_FACTOR);
+ Coordinate ptA = centerPt.getCoordinate();
+ /**
+ * If LEC centre is in B, the max distance is zero, so return null.
+ * This will cause the computed segment distance to be returned,
+ * which is preferred since it occurs on a vertex or edge.
+ */
+ if (targetDistance.isInterior(ptA)) {
+ return null;
+ }
+ Coordinate ptB = targetDistance.nearestFacetPoint(ptA);
+ return pair(ptA, ptB);
+ }
+
+ /**
+ * Creates the priority queue for segments.
+ * Segments which are interior to a polygonal target geometry
+ * are not added to the queue.
+ * So if a query geometry is fully covered by the target
+ * the returned queue is empty.
+ *
+ * @param geom
+ * @return the segment priority queue
+ */
+ private PriorityQueue createSegQueue(Geometry geom) {
+ PriorityQueue priq = new PriorityQueue();
+ geom.apply(new GeometryComponentFilter() {
+
+ @Override
+ public void filter(Geometry geom) {
+ if (geom instanceof LineString) {
+ addSegments(geom.getCoordinates(), priq);
+ }
+ }
+ });
+ return priq;
+ }
+
+ /**
+ * Add segments to queue
+ *
+ * @param pts
+ * @param priq
+ */
+ private void addSegments(Coordinate[] pts, PriorityQueue priq) {
+ DHDSegment segMaxDist = null;
+ DHDSegment prevSeg = null;
+ for (int i = 0; i < pts.length - 1; i++) {
+ DHDSegment seg;
+ if (i == 0) {
+ seg = DHDSegment.create(pts[i], pts[i + 1], targetDistance);
+ }
+ else {
+ //-- avoiding recomputing prev pt distance
+ seg = DHDSegment.create(prevSeg, pts[i + 1], targetDistance);
+ }
+ prevSeg = seg;
+
+ //-- don't add segment if it can't be further away then current max
+ if (segMaxDist == null
+ || seg.getMaxDistanceBound() > segMaxDist.getMaxDistance()) {
+ /**
+ * Don't add interior segments, since their distance must be zero.
+ */
+ addNonInterior(seg, priq);
+ }
+
+ if (segMaxDist == null
+ || seg.getMaxDistance() > segMaxDist.getMaxDistance()) {
+ segMaxDist = seg;;
+ }
+ //System.out.println(seg.distance());
+ }
+ }
+
+ private static class TargetDistance {
+ private IndexedFacetDistance distanceToFacets;
+ private boolean isArea;
+ private IndexedPointInPolygonsLocator ptInArea;
+
+ public TargetDistance(Geometry geom) {
+ distanceToFacets = new IndexedFacetDistance(geom);
+ isArea = geom.getDimension() >= Dimension.A;
+ if (isArea) {
+ ptInArea = new IndexedPointInPolygonsLocator(geom);
+ }
+ }
+
+ public CoordinateSequenceLocation nearestLocation(Coordinate p) {
+ return distanceToFacets.nearestLocation(p);
+ }
+
+ public Coordinate nearestFacetPoint(Coordinate p) {
+ return distanceToFacets.nearestPoint(p);
+ }
+
+ public Coordinate nearestPoint(Coordinate p) {
+ if (ptInArea != null) {
+ if (ptInArea.locate(p) != Location.EXTERIOR) {
+ return p;
+ }
+ }
+ Coordinate nearestPt = distanceToFacets.nearestPoint(p);
+ return nearestPt;
+ }
+
+ public boolean isInterior(Coordinate p) {
+ if (! isArea) return false;
+ return ptInArea.locate(p) == Location.INTERIOR;
+ }
+
+ public boolean isInterior(Coordinate p0, Coordinate p1) {
+ if (! isArea)
+ return false;
+ //-- compute distance to B linework
+ double segDist = distanceToFacets.distance(p0, p1);
+ //-- if segment touches B linework it is not in interior
+ if (segDist == 0)
+ return false;
+ //-- only need to test one point to check interior
+ boolean isInterior = isInterior(p0);
+ return isInterior;
+ }
+
+ }
+
+ private static class DHDSegment implements Comparable {
+
+ public static DHDSegment create(Coordinate p0, Coordinate p1, TargetDistance dist) {
+ DHDSegment seg = new DHDSegment(p0, p1);
+ seg.init(dist);
+ return seg;
+ }
+
+ public static DHDSegment create(DHDSegment prevSeg, Coordinate p1, TargetDistance dist) {
+ DHDSegment seg = new DHDSegment(prevSeg.p1, p1);
+ seg.init(prevSeg.nearPt1, dist);
+ return seg;
+ }
+
+ private Coordinate p0;
+ private Coordinate nearPt0;
+ private Coordinate p1;
+ private Coordinate nearPt1;
+ private double maxDistanceBound = Double.NEGATIVE_INFINITY;
+ private double maxDistance;
+
+ private DHDSegment(Coordinate p0, Coordinate p1) {
+ this.p0 = p0;
+ this.p1 = p1;
+ }
+
+ private DHDSegment(Coordinate p0, Coordinate nearPt0, Coordinate p1, Coordinate nearPt1) {
+ this.p0 = p0;
+ this.nearPt0 = nearPt0;
+ this.p1 = p1;
+ this.nearPt1 = nearPt1;
+ computeMaxDistances();
+ }
+
+ private void init(TargetDistance dist) {
+ nearPt0 = dist.nearestPoint(p0);
+ nearPt1 = dist.nearestPoint(p1);
+ computeMaxDistances();
+ }
+
+ private void init(Coordinate nearest0, TargetDistance dist) {
+ nearPt0 = nearest0;
+ nearPt1 = dist.nearestPoint(p1);
+ computeMaxDistances();
+ }
+
+ public Coordinate getEndpoint(int index) {
+ return index == 0 ? p0 : p1;
+ }
+
+ public double getLength() {
+ return p0.distance(p1);
+ }
+
+ public double getMaxDistance() {
+ return maxDistance;
+ }
+
+ public double getMaxDistanceBound() {
+ return maxDistanceBound;
+ }
+
+ public Coordinate[] getMaxDistPts() {
+ double dist0 = p0.distance(nearPt0);
+ double dist1 = p1.distance(nearPt1);
+ if (dist0 > dist1) {
+ return pair(p0, nearPt0);
+ }
+ return pair(p1, nearPt1);
+ }
+
+ /**
+ * Computes a least upper bound for the maximum distance to a segment.
+ */
+ private void computeMaxDistances() {
+ /**
+ * Least upper bound is the max distance to the endpoints,
+ * plus half segment length.
+ */
+ double dist0 = p0.distance(nearPt0);
+ double dist1 = p1.distance(nearPt1);
+ maxDistance = Math.max(dist0, dist1);
+ maxDistanceBound = maxDistance + getLength() / 2;
+ }
+
+ public DHDSegment[] bisect(TargetDistance dist) {
+ Coordinate mid = new Coordinate(
+ (p0.x + p1.x) / 2,
+ (p0.y + p1.y) / 2
+ );
+ Coordinate nearPtMid = dist.nearestPoint(mid);
+ return new DHDSegment[] {
+ new DHDSegment(p0, nearPt0, mid, nearPtMid ),
+ new DHDSegment(mid, nearPtMid, p1, nearPt1)
+ };
+ }
+
+ /**
+ * For maximum efficiency sort the PriorityQueue with largest maxDistance at front.
+ * Since Java PQ sorts least-first, need to invert the comparison
+ */
+ public int compareTo(DHDSegment o) {
+ return -Double.compare(maxDistanceBound, o.maxDistanceBound);
+ }
+
+ public String toString() {
+ return WKTWriter.toLineString(p0, p1);
+ }
+ }
+}
diff --git a/modules/core/src/main/java/org/locationtech/jts/algorithm/distance/DiscreteHausdorffDistance.java b/modules/core/src/main/java/org/locationtech/jts/algorithm/distance/DiscreteHausdorffDistance.java
index 46a71801cf..b39d923e1f 100644
--- a/modules/core/src/main/java/org/locationtech/jts/algorithm/distance/DiscreteHausdorffDistance.java
+++ b/modules/core/src/main/java/org/locationtech/jts/algorithm/distance/DiscreteHausdorffDistance.java
@@ -71,6 +71,7 @@
* This is more efficient than testing whether A is covered by a buffer of B.
*
* @see DiscreteFrechetDistance
+ * @see DirectedHausdorffDistance
*
*/
public class DiscreteHausdorffDistance
diff --git a/modules/core/src/main/java/org/locationtech/jts/algorithm/construct/IndexedPointInPolygonsLocator.java b/modules/core/src/main/java/org/locationtech/jts/algorithm/locate/IndexedPointInPolygonsLocator.java
similarity index 86%
rename from modules/core/src/main/java/org/locationtech/jts/algorithm/construct/IndexedPointInPolygonsLocator.java
rename to modules/core/src/main/java/org/locationtech/jts/algorithm/locate/IndexedPointInPolygonsLocator.java
index ce99843041..68ec189846 100644
--- a/modules/core/src/main/java/org/locationtech/jts/algorithm/construct/IndexedPointInPolygonsLocator.java
+++ b/modules/core/src/main/java/org/locationtech/jts/algorithm/locate/IndexedPointInPolygonsLocator.java
@@ -9,12 +9,10 @@
*
* http://www.eclipse.org/org/documents/edl-v10.php.
*/
-package org.locationtech.jts.algorithm.construct;
+package org.locationtech.jts.algorithm.locate;
import java.util.List;
-import org.locationtech.jts.algorithm.locate.IndexedPointInAreaLocator;
-import org.locationtech.jts.algorithm.locate.PointOnGeometryLocator;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Envelope;
import org.locationtech.jts.geom.Geometry;
@@ -24,12 +22,15 @@
/**
* Determines the location of a point in the polygonal elements of a geometry.
+ * The polygons may overlap.
* Uses spatial indexing to provide efficient performance.
*
* @author mdavis
+ *
+ * @see IndexedPointInAreaLocator
*
*/
-class IndexedPointInPolygonsLocator implements PointOnGeometryLocator {
+public class IndexedPointInPolygonsLocator implements PointOnGeometryLocator {
private Geometry geom;
private STRtree index;
diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/distance/CoordinateSequenceLocation.java b/modules/core/src/main/java/org/locationtech/jts/operation/distance/CoordinateSequenceLocation.java
new file mode 100644
index 0000000000..d0d03c5a1b
--- /dev/null
+++ b/modules/core/src/main/java/org/locationtech/jts/operation/distance/CoordinateSequenceLocation.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (c) 2026 Martin Davis
+ *
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License 2.0
+ * and Eclipse Distribution License v. 1.0 which accompanies this distribution.
+ * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html
+ * and the Eclipse Distribution License is available at
+ *
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ */
+package org.locationtech.jts.operation.distance;
+
+import org.locationtech.jts.geom.Coordinate;
+import org.locationtech.jts.geom.CoordinateSequence;
+import org.locationtech.jts.geom.CoordinateSequences;
+
+/**
+ * A location on a {@link FacetSequence}.
+ *
+ * Location indexes are always the index of a sequence segment.
+ * Thus they are always less than the number of vertices
+ * in the sequence. The endpoint in a sequence
+ * has the index of the final segment in the sequence.
+ * if the sequence is a ring, the index of the final endpoint is
+ * normalized to 0.
+ *
+ * @author mdavis
+ *
+ */
+public class CoordinateSequenceLocation {
+ private CoordinateSequence seq;
+ private int index;
+ private Coordinate pt;
+
+ public CoordinateSequenceLocation(CoordinateSequence seq, int index, Coordinate pt) {
+ this.seq = seq;
+ this.pt = pt;
+ this.index = index;
+ if (index >= seq.size())
+ this.index = seq.size() - 1;
+ }
+
+ public Coordinate getCoordinate() {
+ return pt;
+ }
+
+ public boolean isSameSegment(CoordinateSequenceLocation f) {
+ if (seq != f.seq)
+ return false;
+ if (index == f.index)
+ return true;
+ //-- check for end pt same as start point of next segment
+ if (isNext(index, f.index)) {
+ Coordinate endPt = seq.getCoordinate(index + 1);
+ return f.pt.equals2D(endPt);
+ }
+ if (isNext(f.index, index)) {
+ Coordinate endPt = f.seq.getCoordinate(index + 1);
+ return pt.equals2D(endPt);
+ }
+ return false;
+ }
+
+ private boolean isNext(int index, int index1) {
+ if (index1 == index + 1)
+ return true;
+ if (index1 == 0 && CoordinateSequences.isRing(seq)
+ && index1 == seq.size() - 1) {
+ return true;
+ }
+ return false;
+ }
+
+ public int getIndex() {
+ return index;
+ }
+
+ public Coordinate getEndPoint(int i) {
+ if (i == 0)
+ return seq.getCoordinate(index);
+ else
+ return seq.getCoordinate(index + 1);
+ }
+
+ public int normalize(int index) {
+ if (index >= seq.size() - 1
+ && CoordinateSequences.isRing(seq)) {
+ return 0;
+ }
+ return index;
+ }
+}
diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/distance/FacetSequence.java b/modules/core/src/main/java/org/locationtech/jts/operation/distance/FacetSequence.java
index 44c587189a..efe70c3b0d 100644
--- a/modules/core/src/main/java/org/locationtech/jts/operation/distance/FacetSequence.java
+++ b/modules/core/src/main/java/org/locationtech/jts/operation/distance/FacetSequence.java
@@ -15,6 +15,7 @@
import org.locationtech.jts.algorithm.Distance;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.CoordinateSequence;
+import org.locationtech.jts.geom.CoordinateSequences;
import org.locationtech.jts.geom.Envelope;
import org.locationtech.jts.geom.LineSegment;
@@ -151,6 +152,57 @@ else if (isPointOther) {
return locs;
}
+ public CoordinateSequenceLocation nearestLocation(Coordinate p)
+ {
+ if (isPoint()) {
+ return new CoordinateSequenceLocation(pts, 0, pts.getCoordinate(0));
+ }
+ return nearestLocationOnLine(p);
+ }
+
+ private CoordinateSequenceLocation nearestLocationOnLine(Coordinate pt)
+ {
+ double minDistance = Double.MAX_VALUE;
+ int index = -1;
+ Coordinate nearestPt = null;
+
+ for (int i = start; i < end - 1; i++) {
+ Coordinate q0 = pts.getCoordinate(i);
+ Coordinate q1 = pts.getCoordinate(i + 1);
+ double dist = Distance.pointToSegment(pt, q0, q1);
+ if (dist < minDistance) {
+ minDistance = dist;
+ nearestPt = nearestPoint(pt, q0, q1);
+ index = i;
+ //-- segments are half-open, so 2nd endpoint belongs to next segment
+ //-- except for last segment on non-closed sequence
+ if (dist == 0.0 && pt.equals2D(q1)) {
+ if (index < pts.size() - 1) {
+ index++;
+ }
+ //-- normalize index for a ring
+ index = normalize(pts, index);
+ }
+ if (minDistance <= 0.0)
+ break;
+ }
+ }
+ return new CoordinateSequenceLocation(pts, index, nearestPt);
+ }
+
+ private static int normalize(CoordinateSequence pts, int index) {
+ if (index >= pts.size() - 1
+ && CoordinateSequences.isRing(pts)) {
+ index = 0;
+ }
+ return index;
+ }
+
+ private Coordinate nearestPoint(Coordinate pt, Coordinate q0, Coordinate q1) {
+ LineSegment seg = new LineSegment(q0, q1);
+ return seg.closestPoint(pt);
+ }
+
private double computeDistanceLineLine(FacetSequence facetSeq, Coordinate[] locs)
{
// both linear - compute minimum segment-segment distance
diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/distance/IndexedFacetDistance.java b/modules/core/src/main/java/org/locationtech/jts/operation/distance/IndexedFacetDistance.java
index e212ef4785..bd8ff34ea4 100644
--- a/modules/core/src/main/java/org/locationtech/jts/operation/distance/IndexedFacetDistance.java
+++ b/modules/core/src/main/java/org/locationtech/jts/operation/distance/IndexedFacetDistance.java
@@ -13,17 +13,19 @@
package org.locationtech.jts.operation.distance;
import org.locationtech.jts.geom.Coordinate;
+import org.locationtech.jts.geom.CoordinateSequence;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.Lineal;
import org.locationtech.jts.geom.Polygonal;
import org.locationtech.jts.geom.Puntal;
+import org.locationtech.jts.geom.impl.CoordinateArraySequence;
import org.locationtech.jts.index.strtree.ItemBoundable;
import org.locationtech.jts.index.strtree.ItemDistance;
import org.locationtech.jts.index.strtree.STRtree;
/**
- * Computes the distance between the facets (segments and vertices)
- * of two {@link Geometry}s
+ * Computes the distance to the facets (segments and vertices)
+ * of a {@link Geometry}
* using a Branch-and-Bound algorithm.
* The Branch-and-Bound algorithm operates over a
* traversal of R-trees built
@@ -87,7 +89,7 @@ public static boolean isWithinDistance(Geometry g1, Geometry g2, double distance
*
* @param g1 a geometry
* @param g2 a geometry
- * @return the nearest points on the facets of the geometries
+ * @return the nearest points on the facets of geometry g1 and g2
*/
public static Coordinate[] nearestPoints(Geometry g1, Geometry g2) {
IndexedFacetDistance dist = new IndexedFacetDistance(g1);
@@ -115,22 +117,6 @@ public IndexedFacetDistance(Geometry geom) {
cachedTree = FacetSequenceTreeBuilder.build(geom);
}
- /**
- * Computes the distance from the base geometry to
- * the given geometry.
- *
- * @param g the geometry to compute the distance to
- *
- * @return the computed distance
- */
- public double distance(Geometry g)
- {
- Object[] obj = nearestFacets(g);
- FacetSequence fs1 = (FacetSequence) obj[0];
- FacetSequence fs2 = (FacetSequence) obj[1];
- return fs1.distance(fs2);
- }
-
private Object[] nearestFacets(Geometry g) {
STRtree tree2 = FacetSequenceTreeBuilder.build(g);
Object[] obj = cachedTree.nearestNeighbour(tree2,
@@ -139,11 +125,11 @@ private Object[] nearestFacets(Geometry g) {
}
/**
- * Computes the nearest points on the base geometry
- * and the given geometry.
+ * Computes the nearest points on the target geometry
+ * and another geometry.
*
* @param g the geometry to compute the nearest location to
- * @return the nearest points
+ * @return the nearest points on the target and the argument geometry
*/
public Coordinate[] nearestPoints(Geometry g)
{
@@ -153,8 +139,63 @@ public Coordinate[] nearestPoints(Geometry g)
return fs1.nearestLocations(fs2);
}
+ public CoordinateSequenceLocation nearestLocation(Coordinate p) {
+ CoordinateSequence seq = new CoordinateArraySequence(new Coordinate[] { p });
+ FacetSequence fs = new FacetSequence(seq, 0);
+ Object nearest = cachedTree.nearestNeighbour(fs.getEnvelope(), fs, FACET_SEQ_DIST);
+ FacetSequence fsN = (FacetSequence) nearest;
+ return fsN.nearestLocation(p);
+
+ }
+
/**
- * Tests whether the base geometry lies within
+ * Computes the nearest point on the target geometry
+ * to a point.
+ *
+ * @param p the point coordinate
+ * @return the nearest point on the target geometry
+ */
+ public Coordinate nearestPoint(Coordinate p)
+ {
+ CoordinateSequence seq = new CoordinateArraySequence(new Coordinate[] { p });
+ FacetSequence fs = new FacetSequence(seq, 0);
+ Object nearest = cachedTree.nearestNeighbour(fs.getEnvelope(), fs, FACET_SEQ_DIST);
+ FacetSequence fsN = (FacetSequence) nearest;
+ return fsN.nearestLocation(p).getCoordinate();
+ }
+
+ /**
+ * Computes the distance from the target geometry to
+ * the given geometry.
+ *
+ * @param g the geometry to compute the distance to
+ *
+ * @return the computed distance
+ */
+ public double distance(Geometry g)
+ {
+ Object[] obj = nearestFacets(g);
+ FacetSequence fs1 = (FacetSequence) obj[0];
+ FacetSequence fs2 = (FacetSequence) obj[1];
+ return fs1.distance(fs2);
+ }
+
+ public double distance(Coordinate p) {
+ return p.distance(nearestPoint(p));
+ }
+
+ public double distance(Coordinate p0, Coordinate p1)
+ {
+ CoordinateSequence seq = new CoordinateArraySequence(new Coordinate[] { p0, p1 });
+ FacetSequence fs2 = new FacetSequence(seq, 0, 2);
+ Object nearest = cachedTree.nearestNeighbour(fs2.getEnvelope(), fs2, FACET_SEQ_DIST);
+ FacetSequence fs1 = (FacetSequence) nearest;
+ Coordinate[] loc = fs1.nearestLocations(fs2);
+ return loc[0].distance(loc[1]);
+ }
+
+ /**
+ * Tests whether the target geometry lies within
* a specified distance of the given geometry.
*
* @param g the geometry to test
@@ -181,6 +222,7 @@ public double distance(ItemBoundable item1, ItemBoundable item2) {
return fs1.distance(fs2);
}
}
+
}
diff --git a/modules/core/src/test/java/org/locationtech/jts/algorithm/distance/DirectedHausdorffDistanceTest.java b/modules/core/src/test/java/org/locationtech/jts/algorithm/distance/DirectedHausdorffDistanceTest.java
new file mode 100644
index 0000000000..2e83ef0ff3
--- /dev/null
+++ b/modules/core/src/test/java/org/locationtech/jts/algorithm/distance/DirectedHausdorffDistanceTest.java
@@ -0,0 +1,453 @@
+/*
+ * Copyright (c) 2026 Martin Davis.
+ *
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License 2.0
+ * and Eclipse Distribution License v. 1.0 which accompanies this distribution.
+ * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html
+ * and the Eclipse Distribution License is available at
+ *
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ */
+package org.locationtech.jts.algorithm.distance;
+
+import org.locationtech.jts.geom.Coordinate;
+import org.locationtech.jts.geom.Geometry;
+
+import junit.textui.TestRunner;
+import test.jts.GeometryTestCase;
+
+public class DirectedHausdorffDistanceTest
+extends GeometryTestCase
+{
+ public static void main(String args[]) {
+ TestRunner.run(DirectedHausdorffDistanceTest.class);
+ }
+
+ public DirectedHausdorffDistanceTest(String name) { super(name); }
+
+ //-- empty inputs
+
+ public void testEmptyPoint()
+ {
+ checkDistanceEmpty("POINT EMPTY", "POINT (1 1)");
+ }
+
+ public void testEmptyLine()
+ {
+ checkDistanceEmpty("LINESTRING EMPTY", "LINESTRING (0 0, 2 1)");
+ }
+
+ public void testEmptyPolygon()
+ {
+ checkDistanceEmpty("POLYGON EMPTY", "POLYGON ((1 9, 9 9, 9 1, 1 1, 1 9))");
+ }
+
+ //--------------------------------------
+ //-- extreme and invalid inputs
+
+ public void testZeroTolerancePoint()
+ {
+ checkDistance("POINT (5 5)", "LINESTRING (5 1, 9 5)",
+ 0,
+ "LINESTRING (5 5, 7 3)");
+ }
+
+ public void testZeroToleranceLine()
+ {
+ checkDistance("LINESTRING (1 5, 5 5)", "LINESTRING (5 1, 9 5)",
+ 0,
+ "LINESTRING (1 5, 5 1)");
+ }
+
+ public void testZeroToleranceZeroLengthLineQuery()
+ {
+ checkDistance("LINESTRING (5 5, 5 5)", "LINESTRING (5 1, 9 5)",
+ 0,
+ "LINESTRING (5 5, 7 3)");
+ }
+
+ public void testZeroLengthLineQuery()
+ {
+ checkDistance("LINESTRING (5 5, 5 5)", "LINESTRING (5 1, 9 5)",
+ "LINESTRING (5 5, 7 3)");
+ }
+
+ public void testZeroLengthPolygonQuery()
+ {
+ checkDistance("POLYGON ((5 5, 5 5, 5 5, 5 5))", "LINESTRING (5 1, 9 5)",
+ "LINESTRING (5 5, 7 3)");
+ }
+
+ public void testZeroLengthLineTarget()
+ {
+ checkDistance("POINT (5 5)", "LINESTRING (5 1, 5 1)",
+ "LINESTRING (5 5, 5 1)");
+ }
+
+ public void testNegativeTolerancePoint()
+ {
+ try {
+ checkDistance("POINT (5 5)", "LINESTRING (5 1, 9 5)",
+ -1,
+ "LINESTRING (5 5, 7 3)");
+ fail();
+ }
+ catch (IllegalArgumentException expected) {
+
+ }
+ }
+
+ public void testNegativeToleranceLine()
+ {
+ try {
+ checkDistance("LINESTRING (1 5, 5 5)", "LINESTRING (5 1, 9 5)",
+ -1,
+ "LINESTRING (1 5, 5 1)");
+ fail();
+ }
+ catch (IllegalArgumentException expected) {
+
+ }
+ }
+
+ //--------------------------------------
+
+
+ public void testPointPoint()
+ {
+ checkHausdorff("POINT (0 0)", "POINT (1 1)",
+ "LINESTRING (0 0, 1 1)");
+ }
+
+ public void testPointsPoints()
+ {
+ String a = "MULTIPOINT ((0 1), (2 3), (4 5), (6 6))";
+ String b = "MULTIPOINT ((0.1 0), (1 0), (2 0), (3 0), (4 0), (5 0))";
+ checkDistance(a, b, "LINESTRING (6 6, 5 0)");
+ checkDistance(b, a, "LINESTRING (5 0, 2 3)");
+ checkHausdorff(a, b, "LINESTRING (6 6, 5 0)");
+ }
+
+ public void testPointPolygonInterior()
+ {
+ checkDistance("POINT (3 4)", "POLYGON ((1 9, 9 9, 9 1, 1 1, 1 9))",
+ 0);
+ }
+
+ public void testPointsPolygon()
+ {
+ checkDistance("MULTIPOINT ((4 3), (2 8), (8 5))", "POLYGON ((6 9, 6 4, 9 1, 1 1, 6 9))",
+ "LINESTRING (2 8, 4.426966292134832 6.48314606741573)");
+ }
+
+ public void testLineSegments()
+ {
+ checkHausdorff("LINESTRING (0 0, 2 0)", "LINESTRING (0 0, 2 1)",
+ "LINESTRING (2 0, 2 1)");
+ }
+
+ public void testLineSegments2()
+ {
+ checkHausdorff("LINESTRING (0 0, 2 0)", "LINESTRING (0 1, 1 2, 2 1)",
+ "LINESTRING (1 0, 1 2)");
+ }
+
+ public void testLinePoints()
+ {
+ checkHausdorff("LINESTRING (0 0, 2 0)", "MULTIPOINT (0 2, 1 0, 2 1)",
+ "LINESTRING (0 0, 0 2)");
+ }
+
+ public void testLinesTopoEqual()
+ {
+ checkDistance("MULTILINESTRING ((10 10, 10 90, 40 30), (40 30, 60 80, 90 30, 40 10))",
+ "LINESTRING (10 10, 10 90, 40 30, 60 80, 90 30, 40 10)",
+ 0.0);
+ }
+
+ public void testLinesPolygon()
+ {
+ checkHausdorff("MULTILINESTRING ((1 1, 2 7), (7 1, 9 9))",
+ "POLYGON ((3 7, 6 7, 6 4, 3 4, 3 7))",
+ "LINESTRING (9 9, 6 7)");
+ }
+
+ public void testLinesPolygon2()
+ {
+ String a = "MULTILINESTRING ((2 3, 2 7), (9 1, 9 8, 4 9))";
+ String b = "POLYGON ((3 7, 6 8, 8 2, 3 4, 3 7))";
+ checkDistance(a, b, "LINESTRING (9 8, 6.3 7.1)");
+ checkHausdorff(a, b, "LINESTRING (2 3, 5.5 3)");
+ }
+
+ public void testPolygonLineCrossingBoundaryResult()
+ {
+ checkDistance("POLYGON ((2 8, 8 2, 2 1, 2 8))",
+ "LINESTRING (6 5, 4 7, 0 0, 8 4)",
+ "LINESTRING (2 8, 3.9384615384615387 6.892307692307693)");
+ }
+
+ public void testPolygonLineCrossingInteriorPoint()
+ {
+ checkDistanceStartPtLen("POLYGON ((2 8, 8 2, 2 1, 2 8))",
+ "LINESTRING (6 5, 4 7, 0 0, 9 1)",
+ "LINESTRING (4.555 2.989, 4.828 0.536)", 0.01);
+ }
+
+ public void testPolygonPolygon()
+ {
+ String a = "POLYGON ((2 18, 18 18, 17 3, 2 2, 2 18))";
+ String b = "POLYGON ((1 19, 5 12, 5 3, 14 10, 11 19, 19 19, 20 0, 1 1, 1 19))";
+ checkDistance(b, a, "LINESTRING (20 0, 17 3)");
+ checkDistance(a, b, "LINESTRING (6.6796875 18, 11 19)");
+ checkHausdorff(a, b, "LINESTRING (6.6796875 18, 11 19)");
+ }
+
+ public void testPolygonPolygonHolesNested()
+ {
+ // B is contained in A
+ String a = "POLYGON ((1 19, 19 19, 19 1, 1 1, 1 19), (6 8, 11 14, 15 7, 6 8))";
+ String b = "POLYGON ((2 18, 18 18, 18 2, 2 2, 2 18), (10 17, 3 7, 17 5, 10 17))";
+ checkDistance(a, b, "LINESTRING (9.817138671875 12.58056640625, 7.863620425230705 13.948029178901006)");
+ checkDistance(b, a, 0.0);
+ }
+
+ public void testMultiPolygons()
+ {
+ String a = "MULTIPOLYGON (((1 1, 1 10, 5 1, 1 1)), ((4 17, 9 15, 9 6, 4 17)))";
+ String b = "MULTIPOLYGON (((1 12, 4 13, 8 10, 1 12)), ((3 8, 7 7, 6 2, 3 8)))";
+ checkDistance(a, b, "LINESTRING (1 1, 5.4 3.2)");
+ checkDistanceStartPtLen(b, a,
+ "LINESTRING (2.669921875 12.556640625, 5.446115154109589 13.818546660958905)",
+ 0.01);
+ }
+
+ // Tests that target area interiors have distance = 0
+ public void testLinePolygonCrossing() throws Exception
+ {
+ String wkt1 = "LINESTRING (2 5, 5 10, 6 4)";
+ String wkt2 = "POLYGON ((1 9, 9 9, 9 1, 1 1, 1 9))";
+ checkDistance(wkt1, wkt2, "LINESTRING (5 10, 5 9)");
+ }
+
+ public void testNonVertexResult()
+ {
+ String wkt1 = "LINESTRING (1 1, 5 10, 9 1)";
+ String wkt2 = "LINESTRING (0 10, 0 0, 10 0)";
+
+ checkHausdorff(wkt1, wkt2, "LINESTRING (6.53857421875 6.5382080078125, 6.53857421875 0)");
+ checkDistance(wkt1, wkt2, "LINESTRING (6.53857421875 6.5382080078125, 6.53857421875 0)");
+ }
+
+ public void testDirectedLines()
+ {
+ String wkt1 = "LINESTRING (1 6, 3 5, 1 4)";
+ String wkt2 = "LINESTRING (1 10, 9 5, 1 2)";
+ checkDistance(wkt1, wkt2, "LINESTRING (1 6, 2.797752808988764 8.876404494382022)");
+ checkDistance(wkt2, wkt1, "LINESTRING (9 5, 3 5)");
+ }
+
+ public void testDirectedLines2()
+ {
+ String wkt1 = "LINESTRING (1 6, 3 5, 1 4)";
+ String wkt2 = "LINESTRING (1 3, 1 9, 9 5, 1 1)";
+ checkDistance(wkt1, wkt2, "LINESTRING (3 5, 1 5)");
+ checkDistance(wkt2, wkt1, "LINESTRING (9 5, 3 5)");
+ }
+
+ /**
+ * Tests that segments are detected as interior even for a large tolerance.
+ */
+ public void testInteriorSegmentsLargeTol()
+ {
+ String a = "POLYGON ((4 6, 5 6, 5 5, 4 5, 4 6))";
+ String b = "POLYGON ((1 9, 9 9, 9 1, 1 1, 1 9))";
+ checkDistance(a, b, 2.0, 0.0);
+ }
+
+ /**
+ * Tests that segment endpoint nearest points
+ * which are interior to B have distance 0
+ */
+ public void testInteriorSegmentsSameExterior()
+ {
+ String a = "POLYGON ((1 9, 3 9, 4 5, 5.05 9, 9 9, 9 1, 1 1, 1 9))";
+ String b = "POLYGON ((1 9, 9 9, 9 1, 1 1, 1 9))";
+ checkDistance(a, b, 0.0);
+ }
+
+ //-----------------------------------------------------
+
+ public void testFullyWithinDistanceEmptyPoints()
+ {
+ String a = "POINT EMPTY";
+ String b = "MULTIPOINT ((1 1), (9 9))";
+ checkFullyWithinDistanceEmpty(a, b);
+ }
+
+ public void testFullyWithinDistanceEmptyLine()
+ {
+ String a = "LINESTRING EMPTY";
+ String b = "LINESTRING (9 9, 1 1)";
+ checkFullyWithinDistanceEmpty(a, b);
+ }
+
+ //-- shows withinDistance envelope check not triggering for disconnected A
+ public void testFullyWithinDistancePoints()
+ {
+ String a = "MULTIPOINT ((1 9), (9 1))";
+ String b = "MULTIPOINT ((1 1), (9 9))";
+ checkFullyWithinDistance(a, b, 1, false);
+ checkFullyWithinDistance(a, b, 8.1, true);
+ }
+
+ public void testFullyWithinDistanceDisconnectedLines()
+ {
+ String a = "MULTILINESTRING ((1 9, 2 9), (8 1, 9 1))";
+ String b = "LINESTRING (9 9, 1 1)";
+ checkFullyWithinDistance(a, b, 1, false);
+ checkFullyWithinDistance(a, b, 6, true);
+ checkFullyWithinDistance(b, a, 1, false);
+ checkFullyWithinDistance(b, a, 7.1, true);
+ }
+
+ public void testFullyWithinDistanceDisconnectedPolygons()
+ {
+ String a = "MULTIPOLYGON (((1 9, 2 9, 2 8, 1 8, 1 9)), ((8 2, 9 2, 9 1, 8 1, 8 2)))";
+ String b = "POLYGON ((1 2, 9 9, 2 1, 1 2))";
+ checkFullyWithinDistance(a, b, 1, false);
+ checkFullyWithinDistance(a, b, 5.3, true);
+ checkFullyWithinDistance(b, a, 1, false);
+ checkFullyWithinDistance(b, a, 7.1, true);
+ }
+
+ public void testFullyWithinDistanceLines()
+ {
+ String a = "MULTILINESTRING ((1 1, 3 3), (7 7, 9 9))";
+ String b = "MULTILINESTRING ((1 9, 1 5), (6 4, 8 2))";
+ checkFullyWithinDistance(a, b, 1, false);
+ checkFullyWithinDistance(a, b, 4, false);
+ checkFullyWithinDistance(a, b, 6, true);
+ }
+
+ public void testFullyWithinDistancePolygons()
+ {
+ String a = "POLYGON ((1 4, 4 4, 4 1, 1 1, 1 4))";
+ String b = "POLYGON ((10 10, 10 15, 15 15, 15 10, 10 10))";
+ checkFullyWithinDistance(a, b, 5, false);
+ checkFullyWithinDistance(a, b, 10, false);
+ checkFullyWithinDistance(a, b, 20, true);
+ }
+
+ public void testFullyWithinDistancePolygonsNestedWithHole()
+ {
+ String a = "POLYGON ((2 8, 8 8, 8 2, 2 2, 2 8))";
+ String b = "POLYGON ((1 9, 9 9, 9 1, 1 1, 1 9), (3 7, 7 7, 7 3, 3 3, 3 7))";
+ checkFullyWithinDistance(a, b, 1, false);
+ checkFullyWithinDistance(a, b, 2, true);
+ checkFullyWithinDistance(a, b, 3, true);
+ }
+
+ //======================================================================
+
+ private static final double TOLERANCE = 0.001;
+
+ private void checkHausdorff(String wkt1, String wkt2, String wktExpected)
+ {
+ Geometry g1 = read(wkt1);
+ Geometry g2 = read(wkt2);
+
+ Coordinate[] pts = DirectedHausdorffDistance.hausdorffDistancePoints(g1, g2);
+ Geometry result = g1.getFactory().createLineString(pts);
+ Geometry expected = read(wktExpected);
+ checkEqualExact(expected, result, TOLERANCE);
+ }
+
+ private void checkDistance(String wkt1, String wkt2, String wktExpected) {
+ Geometry g1 = read(wkt1);
+ Geometry g2 = read(wkt2);
+
+ Coordinate[] pts = DirectedHausdorffDistance.distancePoints(g1, g2);
+ Geometry result = g1.getFactory().createLineString(pts);
+ Geometry expected = read(wktExpected);
+ checkEqualExact(expected, result, TOLERANCE);
+ }
+
+ private void checkDistance(String wkt1, String wkt2, double tolerance, String wktExpected) {
+ Geometry g1 = read(wkt1);
+ Geometry g2 = read(wkt2);
+
+ Coordinate[] pts = DirectedHausdorffDistance.distancePoints(g1, g2, tolerance);
+ Geometry result = g1.getFactory().createLineString(pts);
+ Geometry expected = read(wktExpected);
+ checkEqualExact(expected, result, TOLERANCE);
+ }
+
+ private void checkDistanceStartPtLen(String wkt1, String wkt2,
+ String wktExpected, double resultTolerance) {
+ Geometry g1 = read(wkt1);
+ Geometry g2 = read(wkt2);
+
+ Coordinate[] pts = DirectedHausdorffDistance.distancePoints(g1, g2);
+ Geometry result = g1.getFactory().createLineString(pts);
+ Geometry expected = read(wktExpected);
+
+ Coordinate resultPt = result.getCoordinates()[0];
+ Coordinate expectedPt = expected.getCoordinates()[0];
+ checkEqualXY(expectedPt, resultPt, resultTolerance);
+
+ double distResult = result.getLength();
+ double distExpected = expected.getLength();
+ assertEquals(distExpected, distResult, resultTolerance);
+ }
+
+ private void checkDistance(String wkt1, String wkt2, double tolerance,
+ double expectedDistance) {
+ Geometry g1 = read(wkt1);
+ Geometry g2 = read(wkt2);
+
+ double distResult = DirectedHausdorffDistance.distance(g1, g2, tolerance);
+ assertEquals(expectedDistance, distResult, TOLERANCE);
+ }
+
+ private void checkDistance(String wkt1, String wkt2,
+ double expectedDistance) {
+ Geometry g1 = read(wkt1);
+ Geometry g2 = read(wkt2);
+
+ double distResult = DirectedHausdorffDistance.distance(g1, g2);
+ assertEquals(expectedDistance, distResult, TOLERANCE);
+ }
+
+ private void checkFullyWithinDistance(String a, String b, double distance, boolean expected) {
+ Geometry g1 = read(a);
+ Geometry g2 = read(b);
+
+ boolean result = DirectedHausdorffDistance.isFullyWithinDistance(g1, g2, distance);
+ assertEquals(expected, result);
+ }
+
+ private void checkFullyWithinDistanceEmpty(String a, String b) {
+ checkFullyWithinDistance(a, b, 0, false);
+ checkFullyWithinDistance(b, a, 0, false);
+ checkFullyWithinDistance(a, b, 1, false);
+ checkFullyWithinDistance(b, a, 1, false);
+ checkFullyWithinDistance(a, b, 1000, false);
+ checkFullyWithinDistance(b, a, 1000, false);
+ }
+
+ private void checkDistanceEmpty(String a, String b) {
+ Geometry g1 = read(a);
+ Geometry g2 = read(b);
+
+ Coordinate[] resultPts = DirectedHausdorffDistance.distancePoints(g1, g2);
+ assert(resultPts == null);
+
+ double resultDist = DirectedHausdorffDistance.distance(g1, g2);
+ assert(Double.isNaN(resultDist));
+
+ double hausdorffDist = DirectedHausdorffDistance.hausdorffDistance(g1, g2);
+ assert(Double.isNaN(hausdorffDist));
+ }
+}
diff --git a/modules/core/src/test/java/test/jts/perf/operation/distance/HausdorffDistancePerfTest.java b/modules/core/src/test/java/test/jts/perf/operation/distance/HausdorffDistancePerfTest.java
new file mode 100644
index 0000000000..d1d0876d5d
--- /dev/null
+++ b/modules/core/src/test/java/test/jts/perf/operation/distance/HausdorffDistancePerfTest.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (c) 2026 Martin Davis.
+ *
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License 2.0
+ * and Eclipse Distribution License v. 1.0 which accompanies this distribution.
+ * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html
+ * and the Eclipse Distribution License is available at
+ *
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ */
+package test.jts.perf.operation.distance;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.locationtech.jts.algorithm.distance.DirectedHausdorffDistance;
+import org.locationtech.jts.algorithm.distance.DiscreteHausdorffDistance;
+import org.locationtech.jts.geom.Coordinate;
+import org.locationtech.jts.geom.Geometry;
+import org.locationtech.jts.geom.GeometryFactory;
+import org.locationtech.jts.geom.Point;
+import org.locationtech.jts.geom.util.SineStarFactory;
+import org.locationtech.jts.util.Debug;
+
+import test.jts.perf.PerformanceTestCase;
+import test.jts.perf.PerformanceTestRunner;
+
+public class HausdorffDistancePerfTest extends PerformanceTestCase {
+
+ static final int MAX_ITER = 10000;
+
+ static final boolean VERBOSE = false;
+
+ static GeometryFactory geomFact = new GeometryFactory();
+
+ public static void main(String[] args) {
+ PerformanceTestRunner.run(HausdorffDistancePerfTest.class);
+ }
+
+ private Geometry b;
+
+ private List grid;
+
+ private int distance;
+
+ private DirectedHausdorffDistance dhd;
+
+ public HausdorffDistancePerfTest(String name) {
+ super(name);
+ setRunSize(new int[] { 100, 10_000, 40_000, 160_000, 1_000_000 });
+ setRunIterations(1);
+ }
+
+ public void startRun(int npts)
+ {
+ int nCircles = (int) Math.sqrt(npts);
+ System.out.println("\n------- Running with # circles = " + nCircles * nCircles);
+ b = createSineStar(1000, 300, 50);
+ dhd = new DirectedHausdorffDistance(b);
+ grid = createCircleGrid(2000, nCircles);
+ distance = 2000 / nCircles;
+ }
+
+ public void runFullyWithin() {
+ Debug.println(b);
+
+ int iter = 0;
+ for (Geometry g : grid) {
+ iter++;
+ Debug.println(iter + " ----------------");
+ Debug.println(g);
+ checkHausdorff(g, b);
+ //boolean isWithin = DirectedHausdorffDistance.isFullyWithinDistance(g, b, 4 * distance, 0.1);
+ boolean isWithin = dhd.isFullyWithinDistance(g, 4 * distance);
+
+ //if (iter > 10) break;
+ }
+ }
+
+ private void checkHausdorff(Geometry g, Geometry b) {
+ double distDHD = DiscreteHausdorffDistance.distance(g, b, 0.01);
+
+ double distHD = DirectedHausdorffDistance.hausdorffDistance(g, b);
+ //-- performance testing only
+ //double distDHD = distHD;
+
+ double err = Math.abs(distDHD - distHD) / (distDHD + distHD);
+
+ Debug.println(distDHD + " " + distHD + " err = " + err);
+ if (err > .01) {
+ System.out.println("<<<<<<<<<<< ERROR!");
+ }
+
+ checkFullyWithinDistance(g, b);
+ }
+
+ private void checkFullyWithinDistance(Geometry g, Geometry b) {
+ double distDHD = DirectedHausdorffDistance.distance(g, b, 0.01);
+ double tol = distDHD / 1000;
+ boolean isWithin = DirectedHausdorffDistance.isFullyWithinDistance(g, b, 1.05 * distDHD, tol);
+ boolean isBeyond = ! DirectedHausdorffDistance.isFullyWithinDistance(g, b, 0.95 * distDHD, tol);
+ if (! (isWithin && isBeyond)) {
+ System.out.format("ioWithin = %b isBeyond = %b\n", isWithin, isBeyond);
+ System.out.println("<<<<<<<<<<< ERROR!");
+ DirectedHausdorffDistance.isFullyWithinDistance(g, b, 0.75 * distDHD,tol);
+ }
+ }
+
+ private List createCircleGrid(double size, int nSide) {
+ List geoms = new ArrayList();
+
+ double inc = size / nSide;
+
+ for (int i = 0; i < nSide; i++) {
+ for (int j = 0; j < nSide; j++) {
+ Coordinate p = new Coordinate(i * inc, j * inc);
+ Point pt = geomFact.createPoint(p);
+ Geometry buf = pt.buffer(inc);
+ geoms.add(buf);
+ }
+ }
+ return geoms;
+ }
+
+ private Geometry createSineStar(double loc, double size, int nPts)
+ {
+ SineStarFactory gsf = new SineStarFactory(geomFact);
+ gsf.setCentre(new Coordinate(loc, loc));
+ gsf.setSize(size);
+ gsf.setNumPoints(nPts);
+
+ Geometry g = gsf.createSineStar().getBoundary();
+
+ return g;
+ }
+}
diff --git a/modules/core/src/test/java/test/jts/perf/operation/distance/HausdorffDistanceStressTest.java b/modules/core/src/test/java/test/jts/perf/operation/distance/HausdorffDistanceStressTest.java
new file mode 100644
index 0000000000..3b7fb1693b
--- /dev/null
+++ b/modules/core/src/test/java/test/jts/perf/operation/distance/HausdorffDistanceStressTest.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (c) 2026 Martin Davis.
+ *
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License 2.0
+ * and Eclipse Distribution License v. 1.0 which accompanies this distribution.
+ * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html
+ * and the Eclipse Distribution License is available at
+ *
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ */
+package test.jts.perf.operation.distance;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.locationtech.jts.algorithm.distance.DiscreteHausdorffDistance;
+import org.locationtech.jts.algorithm.distance.DirectedHausdorffDistance;
+import org.locationtech.jts.geom.Coordinate;
+import org.locationtech.jts.geom.Geometry;
+import org.locationtech.jts.geom.GeometryFactory;
+import org.locationtech.jts.geom.Point;
+import org.locationtech.jts.geom.util.SineStarFactory;
+import org.locationtech.jts.util.Debug;
+
+public class HausdorffDistanceStressTest {
+ static final int MAX_ITER = 10000;
+
+ static final boolean VERBOSE = false;
+
+ static GeometryFactory geomFact = new GeometryFactory();
+
+ public static void main(String[] args) {
+
+ HausdorffDistanceStressTest test = new HausdorffDistanceStressTest();
+ test.run();
+ }
+
+ private void run() {
+ Geometry b = createSineStar(1000, 300, 50);
+ Debug.println(b);
+
+ List grid = createCircleGrid(2000, 100);
+
+ int iter = 0;
+ for (Geometry g : grid) {
+ iter++;
+ Debug.println(iter + " ----------------");
+ Debug.println(g);
+ checkHausdorff(g, b);
+ checkFullyWithinDistance(g, b);
+
+ //if (iter > 10) break;
+ }
+ }
+
+ private void checkHausdorff(Geometry g, Geometry b) {
+ double distDHD = DiscreteHausdorffDistance.distance(g, b, 0.01);
+
+ double distHD = DirectedHausdorffDistance.hausdorffDistance(g, b);
+ //-- performance testing only
+ //double distDHD = distHD;
+
+ double err = Math.abs(distDHD - distHD) / (distDHD + distHD);
+
+ Debug.println(distDHD + " " + distHD + " err = " + err);
+ if (err > .01) {
+ System.out.println("<<<<<<<<<<< ERROR!");
+ }
+ }
+
+ private void checkFullyWithinDistance(Geometry g, Geometry b) {
+ double distDHD = DirectedHausdorffDistance.distance(g, b, 0.01);
+
+ boolean isWithin = DirectedHausdorffDistance.isFullyWithinDistance(g, b, 1.05 * distDHD);
+ boolean isBeyond = ! DirectedHausdorffDistance.isFullyWithinDistance(g, b, 0.95 * distDHD);
+
+ if (! (isWithin && isBeyond)) {
+ System.out.format("ioWithin = %b isBeyond = %b\n", isWithin, isBeyond);
+ System.out.println("<<<<<<<<<<< ERROR!");
+ //DirectedHausdorffDistance.isFullyWithinDistance(g, b, 0.75 * distDHD,tol);
+ }
+ }
+
+ private List createCircleGrid(double size, int nSide) {
+ List geoms = new ArrayList();
+
+ double inc = size / nSide;
+
+ for (int i = 0; i < nSide; i++) {
+ for (int j = 0; j < nSide; j++) {
+ Coordinate p = new Coordinate(i * inc, j * inc);
+ Point pt = geomFact.createPoint(p);
+ Geometry buf = pt.buffer(inc);
+ geoms.add(buf);
+ }
+ }
+ return geoms;
+ }
+
+ Geometry createSineStar(double loc, double size, int nPts)
+ {
+ SineStarFactory gsf = new SineStarFactory(geomFact);
+ gsf.setCentre(new Coordinate(loc, loc));
+ gsf.setSize(size);
+ gsf.setNumPoints(nPts);
+
+ Geometry g = gsf.createSineStar().getBoundary();
+
+ return g;
+ }
+}