From b09abfef57ce8c110f74e840151462dd4e11ee33 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Wed, 25 May 2022 11:44:51 -0700 Subject: [PATCH] Fix MinimumDiameter minimumRectangle for flat input (#875) * Fix MinimumDiameter minRectangle for flat input Signed-off-by: Martin Davis --- doc/JTS_Version_History.md | 1 + .../jts/algorithm/MinimumDiameter.java | 69 ++++++++++++++----- .../jts/algorithm/MinimumRectanglelTest.java | 66 ++++++++++++++++++ 3 files changed, 120 insertions(+), 16 deletions(-) create mode 100644 modules/core/src/test/java/org/locationtech/jts/algorithm/MinimumRectanglelTest.java diff --git a/doc/JTS_Version_History.md b/doc/JTS_Version_History.md index 8eb66b9004..6a6cd70693 100644 --- a/doc/JTS_Version_History.md +++ b/doc/JTS_Version_History.md @@ -58,6 +58,7 @@ Distributions for older JTS versions can be obtained at the * Fix IsValidOp for repeated node points (#845) * Fix `IsSimpleOp` for repeated endpoints (#851) * Fix `GeometryFixer` via noding check for zero-distance buffers (#867) +* Fix `MinimumDiameter.minimumRectangle` for flat inputs (#875) # Version 1.18.2 diff --git a/modules/core/src/main/java/org/locationtech/jts/algorithm/MinimumDiameter.java b/modules/core/src/main/java/org/locationtech/jts/algorithm/MinimumDiameter.java index 907fb667bd..91d63375df 100644 --- a/modules/core/src/main/java/org/locationtech/jts/algorithm/MinimumDiameter.java +++ b/modules/core/src/main/java/org/locationtech/jts/algorithm/MinimumDiameter.java @@ -13,6 +13,7 @@ import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryFactory; import org.locationtech.jts.geom.LineSegment; import org.locationtech.jts.geom.LineString; import org.locationtech.jts.geom.LinearRing; @@ -22,10 +23,8 @@ /** * Computes the minimum diameter of a {@link Geometry}. * The minimum diameter is defined to be the - * width of the smallest band that - * contains the geometry, - * where a band is a strip of the plane defined - * by two parallel lines. + * width of the smallest band that contains the geometry, + * where a band is a strip of the plane defined by two parallel lines. * This can be thought of as the smallest hole that the geometry can be * moved through, with a single rotation. *

@@ -33,12 +32,16 @@ * If the input Geometry is known to be convex, a hint can be supplied to * avoid this computation. *

- * This class can also be used to compute a line segment representing - * the minimum diameter, the supporting line segment of the minimum diameter, - * and a minimum rectangle enclosing the input geometry. - * This rectangle will - * have width equal to the minimum diameter, and have one side + * This class can also be used to compute: + *

+ * * * @see ConvexHull * @@ -47,7 +50,14 @@ public class MinimumDiameter { /** - * Gets the minimum rectangle enclosing a geometry. + * Gets the minimum rectangular {@link Polygon} which encloses the input geometry. + * The rectangle has width equal to the minimum diameter, + * and a longer length. + * If the convex hull of the input is degenerate (a line or point) + * a {@link LineString} or {@link Point} is returned. + *

+ * The minimum rectangle can be used as an extremely generalized representation + * for the given geometry. * * @param geom the geometry * @return the minimum rectangle enclosing the geometry @@ -226,6 +236,8 @@ private int findMaxPerpDistance(Coordinate[] pts, LineSegment seg, int startInde maxIndex = nextIndex; nextIndex = nextIndex(pts, maxIndex); + if (nextIndex == startIndex) + break; nextPerpDistance = seg.distancePerpendicular(pts[nextIndex]); } // found maximum width for this segment - update global min dist if appropriate @@ -265,21 +277,18 @@ public Geometry getMinimumRectangle() // check if minimum rectangle is degenerate (a point or line segment) if (minWidth == 0.0) { + //-- Min rectangle is a point if (minBaseSeg.p0.equals2D(minBaseSeg.p1)) { return inputGeom.getFactory().createPoint(minBaseSeg.p0); } - return minBaseSeg.toGeometry(inputGeom.getFactory()); + //-- Min rectangle is a line. Use the diagonal of the extent + return computeMaximumLine(convexHullPts, inputGeom.getFactory()); } // deltas for the base segment of the minimum diameter double dx = minBaseSeg.p1.x - minBaseSeg.p0.x; double dy = minBaseSeg.p1.y - minBaseSeg.p0.y; - /* - double c0 = computeC(dx, dy, minBaseSeg.p0); - double c1 = computeC(dx, dy, minBaseSeg.p1); - */ - double minPara = Double.MAX_VALUE; double maxPara = -Double.MAX_VALUE; double minPerp = Double.MAX_VALUE; @@ -315,6 +324,34 @@ public Geometry getMinimumRectangle() } + /** + * Creates a line of maximum extent from the provided vertices + * @param pts the vertices + * @param factory the geometry factory + * @return the line of maximum extent + */ + private static LineString computeMaximumLine(Coordinate[] pts, GeometryFactory factory) { + //-- find max and min pts for X and Y + Coordinate ptMinX = null; + Coordinate ptMaxX = null; + Coordinate ptMinY = null; + Coordinate ptMaxY = null; + for (Coordinate p : pts) { + if (ptMinX == null || p.getX() < ptMinX.getX()) ptMinX = p; + if (ptMaxX == null || p.getX() > ptMaxX.getX()) ptMaxX = p; + if (ptMinY == null || p.getY() < ptMinY.getY()) ptMinY = p; + if (ptMaxY == null || p.getY() > ptMaxY.getY()) ptMaxY = p; + } + Coordinate p0 = ptMinX; + Coordinate p1 = ptMaxX; + //-- line is vertical - use Y pts + if (p0.getX() == p1.getX()) { + p0 = ptMinY; + p1 = ptMaxY; + } + return factory.createLineString(new Coordinate[] { p0.copy(), p1.copy() }); + } + private static double computeC(double a, double b, Coordinate p) { return a * p.y - b * p.x; diff --git a/modules/core/src/test/java/org/locationtech/jts/algorithm/MinimumRectanglelTest.java b/modules/core/src/test/java/org/locationtech/jts/algorithm/MinimumRectanglelTest.java new file mode 100644 index 0000000000..5538493a0e --- /dev/null +++ b/modules/core/src/test/java/org/locationtech/jts/algorithm/MinimumRectanglelTest.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2022 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; + +import org.locationtech.jts.geom.Geometry; + +import junit.textui.TestRunner; +import test.jts.GeometryTestCase; + + +/** + * @version 1.7 + */ +public class MinimumRectanglelTest extends GeometryTestCase { + + private static final double TOL = 1e-10; + + public static void main(String args[]) { + TestRunner.run(MinimumRectanglelTest.class); + } + + public MinimumRectanglelTest(String name) { super(name); } + + public void testLengthZero() { + checkMinRectangle("LINESTRING (1 1, 1 1)", "POINT (1 1)"); + } + + public void testHorizontal() { + checkMinRectangle("LINESTRING (1 1, 3 1, 5 1, 7 1)", "LINESTRING (1 1, 7 1)"); + } + + public void testVertical() { + checkMinRectangle("LINESTRING (1 1, 1 4, 1 7, 1 9)", "LINESTRING (1 1, 1 9)"); + } + + public void testBentLine() { + checkMinRectangle("LINESTRING (1 2, 3 8, 9 6)", "POLYGON ((9 6, 7 10, -1 6, 1 2, 9 6))"); + } + + /** + * Failure case from https://trac.osgeo.org/postgis/ticket/5163 + * @throws Exception + */ + public void testFlatDiagonal() throws Exception { + checkMinRectangle("LINESTRING(-99.48710639268086 34.79029839231914,-99.48370699999998 34.78689899963806,-99.48152167568102 34.784713675318976)", + "LINESTRING (-99.48710639268086 34.79029839231914, -99.48152167568102 34.784713675318976)"); + } + + private void checkMinRectangle(String wkt, String wktExpected) { + Geometry geom = read(wkt); + Geometry actual = MinimumDiameter.getMinimumRectangle(geom); + Geometry expected = read(wktExpected); + checkEqual(expected, actual, TOL); + } + + +}