Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both builders needed this (micycle1 had already forked the grid one on #1191). License headers and DCO are present. Ship it.

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

Comment on lines +68 to +72

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the setter Martin asked for on #1191. java.util.Random is the right type: JTS is still Java 8, so RandomGenerator cannot land here. A second overload can wait for a language bump.

setRandom(null) NPEs on the next getGeometry(). Throw IllegalArgumentException (same style as setExtent rejecting a non-polygon), or document it.

Worth one javadoc sentence that a second getGeometry() continues the generator and does not rewind — the tests always wrap new Random(seed) and hide that.

/**
* Sets a polygonal mask.
*
Expand Down Expand Up @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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();
Comment on lines +149 to +155

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Making this an instance method is the right cut so the circle path uses the same generator. Do not lift Random onto GeometricShapeBuilder — Hilbert / Koch / Sierpinski do not use an RNG. The duplicated setter on this sibling is correct (RandomPointsInGridBuilder does not extend RandomPointsBuilder).

Pre-existing, leave it: this still returns new Coordinate and skips createCoord / the precision model. The grid-cell path already goes through createCoord.

// 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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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));
}
Comment on lines +47 to +57

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same-seed / different-seed plus the polygonal-mask case below is enough to lock #1191. testDifferentSeedProducesDifferentResult is theoretically flaky; with 100 points it will not fire in practice.

Unseeded default is still non-reproducible. The stream is no longer the process-global Math.random() holder — each builder has its own Random — which is the better default and does not change the public contract.


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();
}
}
Original file line number Diff line number Diff line change
@@ -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));
}
Comment on lines +59 to +63

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good that constrained-to-circle is pinned. That is the path that used to call Math.random() from a static helper.


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();
}
}