From 92937de5b40111a4e1372e2a44a8ec0e5c54c5b3 Mon Sep 17 00:00:00 2001 From: MinUk Date: Sun, 12 Jul 2026 21:20:20 +0900 Subject: [PATCH 1/2] Add setRandom(Random) to RandomPointsBuilder for reproducible behavior Allow passing a Random instance to RandomPointsBuilder to enable deterministic point generation. This is useful for testing and development where reproducible random configurations are needed. Closes #1191 Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: MinUk --- .../jts/shape/random/RandomPointsBuilder.java | 19 ++++- .../shape/random/RandomPointsBuilderTest.java | 81 +++++++++++++++++++ 2 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 modules/core/src/test/java/org/locationtech/jts/shape/random/RandomPointsBuilderTest.java diff --git a/modules/core/src/main/java/org/locationtech/jts/shape/random/RandomPointsBuilder.java b/modules/core/src/main/java/org/locationtech/jts/shape/random/RandomPointsBuilder.java index 04d386532e..bed56788d3 100644 --- a/modules/core/src/main/java/org/locationtech/jts/shape/random/RandomPointsBuilder.java +++ b/modules/core/src/main/java/org/locationtech/jts/shape/random/RandomPointsBuilder.java @@ -12,6 +12,8 @@ package org.locationtech.jts.shape.random; +import java.util.Random; + import org.locationtech.jts.algorithm.locate.IndexedPointInAreaLocator; import org.locationtech.jts.algorithm.locate.PointOnGeometryLocator; import org.locationtech.jts.geom.Coordinate; @@ -34,6 +36,7 @@ public class RandomPointsBuilder { protected Geometry maskPoly = null; private PointOnGeometryLocator extentLocator; + private Random random = new Random(); /** * Create a shape factory which will create shapes using the default @@ -55,6 +58,18 @@ public RandomPointsBuilder(GeometryFactory geomFact) super(geomFact); } + /** + * Sets the random number generator used to generate point coordinates. + * This enables reproducible results by providing a {@link Random} + * with a fixed seed. + * + * @param random the random number generator to use + */ + public void setRandom(Random random) + { + this.random = random; + } + /** * Sets a polygonal mask. * @@ -99,8 +114,8 @@ protected Coordinate createCoord(double x, double y) protected Coordinate createRandomCoord(Envelope env) { - double x = env.getMinX() + env.getWidth() * Math.random(); - double y = env.getMinY() + env.getHeight() * Math.random(); + double x = env.getMinX() + env.getWidth() * random.nextDouble(); + double y = env.getMinY() + env.getHeight() * random.nextDouble(); return createCoord(x, y); } diff --git a/modules/core/src/test/java/org/locationtech/jts/shape/random/RandomPointsBuilderTest.java b/modules/core/src/test/java/org/locationtech/jts/shape/random/RandomPointsBuilderTest.java new file mode 100644 index 0000000000..034e1dcd06 --- /dev/null +++ b/modules/core/src/test/java/org/locationtech/jts/shape/random/RandomPointsBuilderTest.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2026 woogi-kim. + * + * 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.shape.random; + +import java.util.Random; + +import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.geom.Geometry; + +import junit.textui.TestRunner; +import test.jts.GeometryTestCase; + +/** + * Tests {@link RandomPointsBuilder}. + * + * @author woogi-kim + * + */ +public class RandomPointsBuilderTest +extends GeometryTestCase { + public static void main(String args[]) { + TestRunner.run(RandomPointsBuilderTest.class); + } + + public RandomPointsBuilderTest(String name) + { + super(name); + } + + public void testDefault() { + RandomPointsBuilder builder = new RandomPointsBuilder(getGeometryFactory()); + builder.setExtent(new Envelope(0, 10, 0, 10)); + builder.setNumPoints(10); + Geometry result = builder.getGeometry(); + assertEquals(10, result.getNumGeometries()); + } + + public void testReproducibleWithSameSeed() { + Geometry result1 = createPoints(42, 100); + Geometry result2 = createPoints(42, 100); + assertTrue(result1.equalsExact(result2)); + } + + public void testDifferentSeedProducesDifferentResult() { + Geometry result1 = createPoints(42, 100); + Geometry result2 = createPoints(99, 100); + assertFalse(result1.equalsExact(result2)); + } + + public void testReproducibleWithPolygonalExtent() { + Geometry mask = read("POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0))"); + Geometry result1 = createPointsInPolygon(mask, 42, 50); + Geometry result2 = createPointsInPolygon(mask, 42, 50); + assertTrue(result1.equalsExact(result2)); + } + + private Geometry createPoints(long seed, int numPts) { + RandomPointsBuilder builder = new RandomPointsBuilder(getGeometryFactory()); + builder.setExtent(new Envelope(0, 10, 0, 10)); + builder.setNumPoints(numPts); + builder.setRandom(new Random(seed)); + return builder.getGeometry(); + } + + private Geometry createPointsInPolygon(Geometry mask, long seed, int numPts) { + RandomPointsBuilder builder = new RandomPointsBuilder(getGeometryFactory()); + builder.setExtent(mask); + builder.setNumPoints(numPts); + builder.setRandom(new Random(seed)); + return builder.getGeometry(); + } +} From d6fd0e1af48c15f9c169ea8de25c891aa4e911d7 Mon Sep 17 00:00:00 2001 From: MinUk Date: Sun, 12 Jul 2026 21:27:14 +0900 Subject: [PATCH 2/2] Add setRandom(Random) to RandomPointsInGridBuilder for reproducible behavior Apply the same change as RandomPointsBuilder to RandomPointsInGridBuilder, replacing Math.random() with an injectable Random instance. Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: MinUk --- .../random/RandomPointsInGridBuilder.java | 27 +++++-- .../random/RandomPointsInGridBuilderTest.java | 81 +++++++++++++++++++ 2 files changed, 102 insertions(+), 6 deletions(-) create mode 100644 modules/core/src/test/java/org/locationtech/jts/shape/random/RandomPointsInGridBuilderTest.java diff --git a/modules/core/src/main/java/org/locationtech/jts/shape/random/RandomPointsInGridBuilder.java b/modules/core/src/main/java/org/locationtech/jts/shape/random/RandomPointsInGridBuilder.java index 3a7df8a96d..29e033c18d 100644 --- a/modules/core/src/main/java/org/locationtech/jts/shape/random/RandomPointsInGridBuilder.java +++ b/modules/core/src/main/java/org/locationtech/jts/shape/random/RandomPointsInGridBuilder.java @@ -12,6 +12,8 @@ package org.locationtech.jts.shape.random; +import java.util.Random; + import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.GeometryFactory; @@ -32,6 +34,7 @@ public class RandomPointsInGridBuilder { private boolean isConstrainedToCircle = false; private double gutterFraction = 0; + private Random random = new Random(); /** * Create a builder which will create shapes using the default @@ -53,6 +56,18 @@ public RandomPointsInGridBuilder(GeometryFactory geomFact) super(geomFact); } + /** + * Sets the random number generator used to generate point coordinates. + * This enables reproducible results by providing a {@link Random} + * with a fixed seed. + * + * @param random the random number generator to use + */ + public void setRandom(Random random) + { + this.random = random; + } + /** * Sets whether generated points are constrained to lie * within a circle contained within each grid cell. @@ -126,18 +141,18 @@ private Coordinate randomPointInCell(double orgX, double orgY, double xLen, doub private Coordinate randomPointInGridCell(double orgX, double orgY, double xLen, double yLen) { - double x = orgX + xLen * Math.random(); - double y = orgY + yLen * Math.random(); + double x = orgX + xLen * random.nextDouble(); + double y = orgY + yLen * random.nextDouble(); return createCoord(x, y); } - private static Coordinate randomPointInCircle(double orgX, double orgY, double width, double height) + private Coordinate randomPointInCircle(double orgX, double orgY, double width, double height) { double centreX = orgX + width/2; double centreY = orgY + height/2; - - double rndAng = 2 * Math.PI * Math.random(); - double rndRadius = Math.random(); + + double rndAng = 2 * Math.PI * random.nextDouble(); + double rndRadius = random.nextDouble(); // use square root of radius, since area is proportional to square of radius double rndRadius2 = Math.sqrt(rndRadius); double rndX = width/2 * rndRadius2 * Math.cos(rndAng); diff --git a/modules/core/src/test/java/org/locationtech/jts/shape/random/RandomPointsInGridBuilderTest.java b/modules/core/src/test/java/org/locationtech/jts/shape/random/RandomPointsInGridBuilderTest.java new file mode 100644 index 0000000000..90a04336bf --- /dev/null +++ b/modules/core/src/test/java/org/locationtech/jts/shape/random/RandomPointsInGridBuilderTest.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2026 woogi-kim. + * + * 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.shape.random; + +import java.util.Random; + +import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.geom.Geometry; + +import junit.textui.TestRunner; +import test.jts.GeometryTestCase; + +/** + * Tests {@link RandomPointsInGridBuilder}. + * + * @author woogi-kim + * + */ +public class RandomPointsInGridBuilderTest +extends GeometryTestCase { + public static void main(String args[]) { + TestRunner.run(RandomPointsInGridBuilderTest.class); + } + + public RandomPointsInGridBuilderTest(String name) + { + super(name); + } + + public void testDefault() { + RandomPointsInGridBuilder builder = new RandomPointsInGridBuilder(getGeometryFactory()); + builder.setExtent(new Envelope(0, 10, 0, 10)); + builder.setNumPoints(9); + Geometry result = builder.getGeometry(); + assertEquals(9, result.getNumGeometries()); + } + + public void testReproducibleWithSameSeed() { + Geometry result1 = createGridPoints(42, 25); + Geometry result2 = createGridPoints(42, 25); + assertTrue(result1.equalsExact(result2)); + } + + public void testDifferentSeedProducesDifferentResult() { + Geometry result1 = createGridPoints(42, 25); + Geometry result2 = createGridPoints(99, 25); + assertFalse(result1.equalsExact(result2)); + } + + public void testReproducibleWithConstrainedToCircle() { + Geometry result1 = createGridPointsInCircle(42, 25); + Geometry result2 = createGridPointsInCircle(42, 25); + assertTrue(result1.equalsExact(result2)); + } + + private Geometry createGridPoints(long seed, int numPts) { + RandomPointsInGridBuilder builder = new RandomPointsInGridBuilder(getGeometryFactory()); + builder.setExtent(new Envelope(0, 10, 0, 10)); + builder.setNumPoints(numPts); + builder.setRandom(new Random(seed)); + return builder.getGeometry(); + } + + private Geometry createGridPointsInCircle(long seed, int numPts) { + RandomPointsInGridBuilder builder = new RandomPointsInGridBuilder(getGeometryFactory()); + builder.setExtent(new Envelope(0, 10, 0, 10)); + builder.setNumPoints(numPts); + builder.setConstrainedToCircle(true); + builder.setRandom(new Random(seed)); + return builder.getGeometry(); + } +}