LUCENE-8086: spatial-extras Geo3dFactory: Use GeoExactCircle with configurable precision for non-spherical planet models.

Some internal refactorings as well.
This commit is contained in:
David Smiley 2017-12-13 16:32:48 -05:00
parent 44cc0defe8
commit d66d9549d7
9 changed files with 261 additions and 350 deletions

View File

@ -72,6 +72,10 @@ Improvements
disk. This change adds an expert setting to opt ouf of this behavior unless disk. This change adds an expert setting to opt ouf of this behavior unless
flusing is falling behind. (Simon Willnauer) flusing is falling behind. (Simon Willnauer)
* LUCENE-8086: spatial-extras Geo3dFactory: Use GeoExactCircle with
configurable precision for non-spherical planet models.
(Ignacio Vera via David Smiley)
======================= Lucene 7.2.0 ======================= ======================= Lucene 7.2.0 =======================
API Changes API Changes

View File

@ -20,13 +20,10 @@ package org.apache.lucene.spatial.spatial4j;
import org.apache.lucene.spatial3d.geom.GeoCircle; import org.apache.lucene.spatial3d.geom.GeoCircle;
import org.apache.lucene.spatial3d.geom.GeoCircleFactory; import org.apache.lucene.spatial3d.geom.GeoCircleFactory;
import org.apache.lucene.spatial3d.geom.GeoPointShapeFactory; import org.apache.lucene.spatial3d.geom.GeoPointShapeFactory;
import org.apache.lucene.spatial3d.geom.PlanetModel;
import org.locationtech.spatial4j.context.SpatialContext; import org.locationtech.spatial4j.context.SpatialContext;
import org.locationtech.spatial4j.distance.DistanceUtils; import org.locationtech.spatial4j.distance.DistanceUtils;
import org.locationtech.spatial4j.shape.Circle; import org.locationtech.spatial4j.shape.Circle;
import org.locationtech.spatial4j.shape.Point; import org.locationtech.spatial4j.shape.Point;
import org.locationtech.spatial4j.shape.Shape;
import org.locationtech.spatial4j.shape.SpatialRelation;
/** /**
* Specialization of a {@link Geo3dShape} which represents a {@link Circle}. * Specialization of a {@link Geo3dShape} which represents a {@link Circle}.
@ -67,16 +64,4 @@ public class Geo3dCircleShape extends Geo3dShape<GeoCircle> implements Circle {
} }
return center; return center;
} }
//TODO Improve GeoCircle to properly relate a point with WGS84 model -- LUCENE-7970
@Override
public SpatialRelation relate(Shape other) {
if (shape.getPlanetModel() != PlanetModel.SPHERE && other instanceof Point) {
if (spatialcontext.getDistCalc().distance((Point) other, getCenter()) <= getRadius()) {
return SpatialRelation.CONTAINS;
}
return SpatialRelation.DISJOINT;
}
return super.relate(other);
}
} }

View File

@ -73,62 +73,20 @@ public class Geo3dDistanceCalculator implements DistanceCalculator {
@Override @Override
public Point pointOnBearing(Point from, double distDEG, double bearingDEG, SpatialContext ctx, Point reuse) { public Point pointOnBearing(Point from, double distDEG, double bearingDEG, SpatialContext ctx, Point reuse) {
// Algorithm using Vincenty's formulae (https://en.wikipedia.org/wiki/Vincenty%27s_formulae)
// which takes into account that planets may not be spherical.
//Code adaptation from http://www.movable-type.co.uk/scripts/latlong-vincenty.html
Geo3dPointShape geoFrom = (Geo3dPointShape) from; Geo3dPointShape geoFrom = (Geo3dPointShape) from;
GeoPoint point = (GeoPoint) geoFrom.shape; GeoPoint point = (GeoPoint) geoFrom.shape;
double lat = point.getLatitude();
double lon = point.getLongitude();
double dist = DistanceUtils.DEGREES_TO_RADIANS * distDEG; double dist = DistanceUtils.DEGREES_TO_RADIANS * distDEG;
double bearing = DistanceUtils.DEGREES_TO_RADIANS * bearingDEG; double bearing = DistanceUtils.DEGREES_TO_RADIANS * bearingDEG;
GeoPoint newPoint = planetModel.surfacePointOnBearing(point, dist, bearing);
double sinα1 = Math.sin(bearing); double newLat = newPoint.getLatitude() * DistanceUtils.RADIANS_TO_DEGREES;
double cosα1 = Math.cos(bearing); double newLon = newPoint.getLongitude() * DistanceUtils.RADIANS_TO_DEGREES;
if (reuse != null) {
double tanU1 = (1 - planetModel.flattening) * Math.tan(lat); reuse.reset(newLon, newLat);
double cosU1 = 1 / Math.sqrt((1 + tanU1 * tanU1)); return reuse;
double sinU1 = tanU1 * cosU1; }
else {
double σ1 = Math.atan2(tanU1, cosα1); return ctx.getShapeFactory().pointXY(newLon, newLat);
double sinα = cosU1 * sinα1;
double cosSqα = 1 - sinα * sinα;
double uSq = cosSqα * planetModel.squareRatio;// (planetModel.ab* planetModel.ab - planetModel.c*planetModel.c) / (planetModel.c*planetModel.c);
double A = 1 + uSq / 16384 * (4096 + uSq * (-768 + uSq * (320 - 175 * uSq)));
double B = uSq / 1024 * (256 + uSq * (-128 + uSq * (74 - 47 * uSq)));
double cos2σM;
double sinσ;
double cosσ;
double Δσ;
double σ = dist / (planetModel.c * A);
double σʹ;
double iterations = 0;
do {
cos2σM = Math.cos(2 * σ1 + σ);
sinσ = Math.sin(σ);
cosσ = Math.cos(σ);
Δσ = B * sinσ * (cos2σM + B / 4 * (cosσ * (-1 + 2 * cos2σM * cos2σM) -
B / 6 * cos2σM * (-3 + 4 * sinσ * sinσ) * (-3 + 4 * cos2σM * cos2σM)));
σʹ = σ;
σ = dist / (planetModel.c * A) + Δσ;
} while (Math.abs(σ - σʹ) > 1e-12 && ++iterations < 200);
if (iterations >= 200) {
throw new RuntimeException("Formula failed to converge");
} }
double x = sinU1 * sinσ - cosU1 * cosσ * cosα1;
double φ2 = Math.atan2(sinU1 * cosσ + cosU1 * sinσ * cosα1, (1 - planetModel.flattening) * Math.sqrt(sinα * sinα + x * x));
double λ = Math.atan2(sinσ * sinα1, cosU1 * cosσ - sinU1 * sinσ * cosα1);
double C = planetModel.flattening / 16 * cosSqα * (4 + planetModel.flattening * (4 - 3 * cosSqα));
double L = λ - (1 - C) * planetModel.flattening * sinα *
(σ + C * sinσ * (cos2σM + C * cosσ * (-1 + 2 * cos2σM * cos2σM)));
double λ2 = (lon + L + 3 * Math.PI) % (2 * Math.PI) - Math.PI; // normalise to -180..+180
return ctx.getShapeFactory().pointXY(λ2 * DistanceUtils.RADIANS_TO_DEGREES,
φ2 * DistanceUtils.RADIANS_TO_DEGREES);
} }
@Override @Override

View File

@ -112,11 +112,7 @@ public class Geo3dShape<T extends GeoAreaShape> implements Shape {
if (bbox == null) { if (bbox == null) {
LatLonBounds bounds = new LatLonBounds(); LatLonBounds bounds = new LatLonBounds();
shape.getBounds(bounds); shape.getBounds(bounds);
double leftLon = bounds.checkNoLongitudeBound() ? -Math.PI : bounds.getLeftLongitude(); GeoBBox geoBBox = GeoBBoxFactory.makeGeoBBox(shape.getPlanetModel(), bounds);
double rightLon = bounds.checkNoLongitudeBound() ? Math.PI : bounds.getRightLongitude();
double minLat = bounds.checkNoBottomLatitudeBound() ? -Math.PI * 0.5 : bounds.getMinLatitude();
double maxLat = bounds.checkNoTopLatitudeBound() ? Math.PI * 0.5 : bounds.getMaxLatitude();
GeoBBox geoBBox = GeoBBoxFactory.makeGeoBBox(shape.getPlanetModel(), maxLat, minLat, leftLon, rightLon);
bbox = new Geo3dRectangleShape(geoBBox, spatialcontext); bbox = new Geo3dRectangleShape(geoBBox, spatialcontext);
this.boundingBox = bbox; this.boundingBox = bbox;
} }

View File

@ -55,6 +55,13 @@ public class Geo3dShapeFactory implements ShapeFactory {
private SpatialContext context; private SpatialContext context;
private PlanetModel planetModel; private PlanetModel planetModel;
/**
* Default accuracy for circles when not using the unit sphere.
* It is equivalent to ~10m on the surface of the earth.
*/
private static final double DEFAULT_CIRCLE_ACCURACY = 1e-4;
private double circleAccuracy = DEFAULT_CIRCLE_ACCURACY;
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public Geo3dShapeFactory(SpatialContext context, SpatialContextFactory factory) { public Geo3dShapeFactory(SpatialContext context, SpatialContextFactory factory) {
this.context = context; this.context = context;
@ -67,6 +74,16 @@ public class Geo3dShapeFactory implements ShapeFactory {
return context; return context;
} }
/**
* Set the accuracy for circles in decimal degrees. Note that accuracy has no effect
* when the planet model is a sphere. In that case, circles are always fully precise.
*
* @param circleAccuracy the provided accuracy in decimal degrees.
*/
public void setCircleAccuracy(double circleAccuracy) {
this.circleAccuracy = circleAccuracy;
}
@Override @Override
public boolean isNormWrapLongitude() { public boolean isNormWrapLongitude() {
return normWrapLongitude; return normWrapLongitude;
@ -150,10 +167,23 @@ public class Geo3dShapeFactory implements ShapeFactory {
@Override @Override
public Circle circle(double x, double y, double distance) { public Circle circle(double x, double y, double distance) {
GeoCircle circle = GeoCircleFactory.makeGeoCircle(planetModel, GeoCircle circle;
if (planetModel.isSphere()) {
circle = GeoCircleFactory.makeGeoCircle(planetModel,
y * DistanceUtils.DEGREES_TO_RADIANS, y * DistanceUtils.DEGREES_TO_RADIANS,
x * DistanceUtils.DEGREES_TO_RADIANS, x * DistanceUtils.DEGREES_TO_RADIANS,
distance * DistanceUtils.DEGREES_TO_RADIANS); distance * DistanceUtils.DEGREES_TO_RADIANS);
}
else {
//accuracy is defined as a linear distance in this class. At tiny distances, linear distance
//can be approximated to surface distance in radians.
circle = GeoCircleFactory.makeExactGeoCircle(planetModel,
y * DistanceUtils.DEGREES_TO_RADIANS,
x * DistanceUtils.DEGREES_TO_RADIANS,
distance * DistanceUtils.DEGREES_TO_RADIANS,
circleAccuracy * DistanceUtils.DEGREES_TO_RADIANS);
}
return new Geo3dCircleShape(circle, context); return new Geo3dCircleShape(circle, context);
} }
@ -238,8 +268,7 @@ public class Geo3dShapeFactory implements ShapeFactory {
/** /**
* Geo3d implementation of {@link org.locationtech.spatial4j.shape.ShapeFactory.LineStringBuilder} to generate * Geo3d implementation of {@link org.locationtech.spatial4j.shape.ShapeFactory.LineStringBuilder} to generate
* nine Strings. Note that GeoPath needs a buffer so we set the * line strings.
* buffer to 1e-10.
*/ */
private class Geo3dLineStringBuilder extends Geo3dPointBuilder<LineStringBuilder> implements LineStringBuilder { private class Geo3dLineStringBuilder extends Geo3dPointBuilder<LineStringBuilder> implements LineStringBuilder {
@ -373,7 +402,7 @@ public class Geo3dShapeFactory implements ShapeFactory {
/** /**
* Geo3d implementation of {@link org.locationtech.spatial4j.shape.ShapeFactory.MultiShapeBuilder} to generate * Geo3d implementation of {@link org.locationtech.spatial4j.shape.ShapeFactory.MultiShapeBuilder} to generate
* geometry collections * geometry collections.
* *
* @param <T> is the type of shapes. * @param <T> is the type of shapes.
*/ */

View File

@ -1,264 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.lucene.spatial.spatial4j;
import java.util.ArrayList;
import java.util.List;
import org.apache.lucene.spatial3d.geom.GeoPath;
import org.apache.lucene.spatial3d.geom.GeoPolygon;
import org.locationtech.spatial4j.TestLog;
import org.locationtech.spatial4j.context.SpatialContext;
import org.locationtech.spatial4j.distance.DistanceUtils;
import org.locationtech.spatial4j.shape.Circle;
import org.locationtech.spatial4j.shape.Point;
import org.locationtech.spatial4j.shape.RectIntersectionTestHelper;
import org.apache.lucene.spatial3d.geom.LatLonBounds;
import org.apache.lucene.spatial3d.geom.GeoBBox;
import org.apache.lucene.spatial3d.geom.GeoBBoxFactory;
import org.apache.lucene.spatial3d.geom.GeoCircle;
import org.apache.lucene.spatial3d.geom.GeoCircleFactory;
import org.apache.lucene.spatial3d.geom.GeoPathFactory;
import org.apache.lucene.spatial3d.geom.GeoPoint;
import org.apache.lucene.spatial3d.geom.GeoPolygonFactory;
import org.apache.lucene.spatial3d.geom.GeoShape;
import org.apache.lucene.spatial3d.geom.PlanetModel;
import org.junit.Rule;
import org.junit.Test;
import static org.locationtech.spatial4j.distance.DistanceUtils.DEGREES_TO_RADIANS;
public abstract class Geo3dShapeRectRelationTestCase extends RandomizedShapeTestCase {
protected final static double RADIANS_PER_DEGREE = Math.PI/180.0;
@Rule
public final TestLog testLog = TestLog.instance;
protected final PlanetModel planetModel;
public Geo3dShapeRectRelationTestCase(PlanetModel planetModel) {
super(SpatialContext.GEO);
this.planetModel = planetModel;
}
protected GeoBBox getBoundingBox(final GeoShape path) {
LatLonBounds bounds = new LatLonBounds();
path.getBounds(bounds);
double leftLon;
double rightLon;
if (bounds.checkNoLongitudeBound()) {
leftLon = -Math.PI;
rightLon = Math.PI;
} else {
leftLon = bounds.getLeftLongitude().doubleValue();
rightLon = bounds.getRightLongitude().doubleValue();
}
double minLat;
if (bounds.checkNoBottomLatitudeBound()) {
minLat = -Math.PI * 0.5;
} else {
minLat = bounds.getMinLatitude().doubleValue();
}
double maxLat;
if (bounds.checkNoTopLatitudeBound()) {
maxLat = Math.PI * 0.5;
} else {
maxLat = bounds.getMaxLatitude().doubleValue();
}
return GeoBBoxFactory.makeGeoBBox(planetModel, maxLat, minLat, leftLon, rightLon);
}
public abstract class Geo3dRectIntersectionTestHelper extends RectIntersectionTestHelper<Geo3dShape> {
public Geo3dRectIntersectionTestHelper(SpatialContext ctx) {
super(ctx);
}
//20 times each -- should be plenty
protected int getContainsMinimum(int laps) {
return 20;
}
protected int getIntersectsMinimum(int laps) {
return 20;
}
// producing "within" cases in Geo3D based on our random shapes doesn't happen often. It'd be nice to increase this.
protected int getWithinMinimum(int laps) {
return 2;
}
protected int getDisjointMinimum(int laps) {
return 20;
}
protected int getBoundingMinimum(int laps) {
return 20;
}
}
@Test
public void testGeoCircleRect() {
new Geo3dRectIntersectionTestHelper(ctx) {
@Override
protected Geo3dShape generateRandomShape(Point nearP) {
final int circleRadius = 180 - random().nextInt(180);//no 0-radius
final Point point = nearP;
final GeoCircle shape = GeoCircleFactory.makeGeoCircle(planetModel, point.getY() * DEGREES_TO_RADIANS, point.getX() * DEGREES_TO_RADIANS,
circleRadius * DEGREES_TO_RADIANS);
return new Geo3dShape(shape, ctx);
}
@Override
protected Point randomPointInEmptyShape(Geo3dShape shape) {
GeoPoint geoPoint = ((GeoCircle)shape.shape).getCenter();
return geoPointToSpatial4jPoint(geoPoint);
}
}.testRelateWithRectangle();
}
@Test
public void testGeoBBoxRect() {
new Geo3dRectIntersectionTestHelper(ctx) {
@Override
protected boolean isRandomShapeRectangular() {
return true;
}
@Override
protected Geo3dShape generateRandomShape(Point nearP) {
// (ignoring nearP)
Point ulhcPoint = randomPoint();
Point lrhcPoint = randomPoint();
if (ulhcPoint.getY() < lrhcPoint.getY()) {
//swap
Point temp = ulhcPoint;
ulhcPoint = lrhcPoint;
lrhcPoint = temp;
}
final GeoBBox shape = GeoBBoxFactory.makeGeoBBox(planetModel, ulhcPoint.getY() * DEGREES_TO_RADIANS,
lrhcPoint.getY() * DEGREES_TO_RADIANS,
ulhcPoint.getX() * DEGREES_TO_RADIANS,
lrhcPoint.getX() * DEGREES_TO_RADIANS);
return new Geo3dShape(shape, ctx);
}
@Override
protected Point randomPointInEmptyShape(Geo3dShape shape) {
return shape.getBoundingBox().getCenter();
}
}.testRelateWithRectangle();
}
@Test
public void testGeoPolygonRect() {
new Geo3dRectIntersectionTestHelper(ctx) {
@Override
protected Geo3dShape generateRandomShape(Point nearP) {
final Point centerPoint = randomPoint();
final int maxDistance = random().nextInt(160) + 20;
final Circle pointZone = ctx.makeCircle(centerPoint, maxDistance);
final int vertexCount = random().nextInt(3) + 3;
while (true) {
final List<GeoPoint> geoPoints = new ArrayList<>();
while (geoPoints.size() < vertexCount) {
final Point point = randomPointIn(pointZone);
final GeoPoint gPt = new GeoPoint(planetModel, point.getY() * DEGREES_TO_RADIANS, point.getX() * DEGREES_TO_RADIANS);
geoPoints.add(gPt);
}
try {
final GeoPolygon shape = GeoPolygonFactory.makeGeoPolygon(planetModel, geoPoints);
if (shape == null) {
continue;
}
return new Geo3dShape(shape, ctx);
} catch (IllegalArgumentException e) {
// This is what happens when we create a shape that is invalid. Although it is conceivable that there are cases where
// the exception is thrown incorrectly, we aren't going to be able to do that in this random test.
continue;
}
}
}
@Override
protected Point randomPointInEmptyShape(Geo3dShape shape) {
throw new IllegalStateException("unexpected; need to finish test code");
}
@Override
protected int getWithinMinimum(int laps) {
// Long/thin so lets just find 1.
return 1;
}
}.testRelateWithRectangle();
}
@Test
public void testGeoPathRect() {
new Geo3dRectIntersectionTestHelper(ctx) {
@Override
protected Geo3dShape generateRandomShape(Point nearP) {
final Point centerPoint = randomPoint();
final int maxDistance = random().nextInt(160) + 20;
final Circle pointZone = ctx.makeCircle(centerPoint, maxDistance);
final int pointCount = random().nextInt(5) + 1;
final double width = (random().nextInt(89)+1) * DEGREES_TO_RADIANS;
final GeoPoint[] points = new GeoPoint[pointCount];
while (true) {
for (int i = 0; i < pointCount; i++) {
final Point nextPoint = randomPointIn(pointZone);
points[i] = new GeoPoint(planetModel, nextPoint.getY() * DEGREES_TO_RADIANS, nextPoint.getX() * DEGREES_TO_RADIANS);
}
try {
final GeoPath path = GeoPathFactory.makeGeoPath(planetModel, width, points);
return new Geo3dShape(path, ctx);
} catch (IllegalArgumentException e) {
// This is what happens when we create a shape that is invalid. Although it is conceivable that there are cases where
// the exception is thrown incorrectly, we aren't going to be able to do that in this random test.
continue;
}
}
}
@Override
protected Point randomPointInEmptyShape(Geo3dShape shape) {
throw new IllegalStateException("unexpected; need to finish test code");
}
@Override
protected int getWithinMinimum(int laps) {
// Long/thin so lets just find 1.
return 1;
}
}.testRelateWithRectangle();
}
private Point geoPointToSpatial4jPoint(GeoPoint geoPoint) {
return ctx.makePoint(geoPoint.getLongitude() * DistanceUtils.RADIANS_TO_DEGREES,
geoPoint.getLatitude() * DistanceUtils.RADIANS_TO_DEGREES);
}
}

View File

@ -34,13 +34,13 @@ import org.locationtech.spatial4j.shape.Point;
import org.locationtech.spatial4j.shape.Rectangle; import org.locationtech.spatial4j.shape.Rectangle;
import org.locationtech.spatial4j.shape.SpatialRelation; import org.locationtech.spatial4j.shape.SpatialRelation;
public class Geo3dShapeSphereModelRectRelationTest extends Geo3dShapeRectRelationTestCase { public class Geo3dShapeSphereModelRectRelationTest extends ShapeRectRelationTestCase {
PlanetModel planetModel = PlanetModel.SPHERE;
public Geo3dShapeSphereModelRectRelationTest() { public Geo3dShapeSphereModelRectRelationTest() {
super(PlanetModel.SPHERE);
Geo3dSpatialContextFactory factory = new Geo3dSpatialContextFactory(); Geo3dSpatialContextFactory factory = new Geo3dSpatialContextFactory();
factory.planetModel = PlanetModel.SPHERE; factory.planetModel = planetModel;
//factory.distCalc = new GeodesicSphereDistCalc.Haversine();
this.ctx = factory.newSpatialContext(); this.ctx = factory.newSpatialContext();
} }

View File

@ -30,14 +30,16 @@ import org.locationtech.spatial4j.shape.Circle;
import org.locationtech.spatial4j.shape.Point; import org.locationtech.spatial4j.shape.Point;
import org.locationtech.spatial4j.shape.SpatialRelation; import org.locationtech.spatial4j.shape.SpatialRelation;
public class Geo3dShapeWGS84ModelRectRelationTest extends Geo3dShapeRectRelationTestCase { public class Geo3dShapeWGS84ModelRectRelationTest extends ShapeRectRelationTestCase {
PlanetModel planetModel = PlanetModel.WGS84;
public Geo3dShapeWGS84ModelRectRelationTest() { public Geo3dShapeWGS84ModelRectRelationTest() {
super(PlanetModel.WGS84);
Geo3dSpatialContextFactory factory = new Geo3dSpatialContextFactory(); Geo3dSpatialContextFactory factory = new Geo3dSpatialContextFactory();
factory.planetModel = PlanetModel.WGS84; factory.planetModel = planetModel;
//factory.distCalc = new GeodesicSphereDistCalc.Haversine();
this.ctx = factory.newSpatialContext(); this.ctx = factory.newSpatialContext();
this.maxRadius = 178;
((Geo3dShapeFactory)ctx.getShapeFactory()).setCircleAccuracy(1e-6);
} }
@Test @Test

View File

@ -0,0 +1,201 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.lucene.spatial.spatial4j;
import org.junit.Rule;
import org.junit.Test;
import org.locationtech.spatial4j.TestLog;
import org.locationtech.spatial4j.context.SpatialContext;
import org.locationtech.spatial4j.shape.Circle;
import org.locationtech.spatial4j.shape.Point;
import org.locationtech.spatial4j.shape.RectIntersectionTestHelper;
import org.locationtech.spatial4j.shape.Shape;
import org.locationtech.spatial4j.shape.ShapeFactory;
import static org.locationtech.spatial4j.distance.DistanceUtils.DEGREES_TO_RADIANS;
public abstract class ShapeRectRelationTestCase extends RandomizedShapeTestCase {
protected final static double RADIANS_PER_DEGREE = Math.PI/180.0;
@Rule
public final TestLog testLog = TestLog.instance;
protected int maxRadius = 180;
public ShapeRectRelationTestCase() {
super(SpatialContext.GEO);
}
public abstract class AbstractRectIntersectionTestHelper extends RectIntersectionTestHelper<Shape> {
public AbstractRectIntersectionTestHelper(SpatialContext ctx) {
super(ctx);
}
//20 times each -- should be plenty
protected int getContainsMinimum(int laps) {
return 20;
}
protected int getIntersectsMinimum(int laps) {
return 20;
}
// producing "within" cases in Geo3D based on our random shapes doesn't happen often. It'd be nice to increase this.
protected int getWithinMinimum(int laps) {
return 2;
}
protected int getDisjointMinimum(int laps) {
return 20;
}
protected int getBoundingMinimum(int laps) {
return 20;
}
}
@Test
public void testGeoCircleRect() {
new AbstractRectIntersectionTestHelper(ctx) {
@Override
protected Shape generateRandomShape(Point nearP) {
final int circleRadius = maxRadius - random().nextInt(maxRadius);//no 0-radius
return ctx.getShapeFactory().circle(nearP, circleRadius);
}
@Override
protected Point randomPointInEmptyShape(Shape shape) {
return shape.getCenter();
}
}.testRelateWithRectangle();
}
@Test
public void testGeoBBoxRect() {
new AbstractRectIntersectionTestHelper(ctx) {
@Override
protected boolean isRandomShapeRectangular() {
return true;
}
@Override
protected Shape generateRandomShape(Point nearP) {
Point upperRight = randomPoint();
Point lowerLeft = randomPoint();
if (upperRight.getY() < lowerLeft.getY()) {
//swap
Point temp = upperRight;
upperRight = lowerLeft;
lowerLeft = temp;
}
return ctx.getShapeFactory().rect(lowerLeft, upperRight);
}
@Override
protected Point randomPointInEmptyShape(Shape shape) {
return shape.getCenter();
}
}.testRelateWithRectangle();
}
@Test
public void testGeoPolygonRect() {
new AbstractRectIntersectionTestHelper(ctx) {
@Override
protected Shape generateRandomShape(Point nearP) {
final Point centerPoint = randomPoint();
final int maxDistance = random().nextInt(maxRadius -20) + 20;
final Circle pointZone = ctx.getShapeFactory().circle(centerPoint, maxDistance);
final int vertexCount = random().nextInt(3) + 3;
while (true) {
ShapeFactory.PolygonBuilder builder = ctx.getShapeFactory().polygon();
for (int i = 0; i < vertexCount; i++) {
final Point point = randomPointIn(pointZone);
builder.pointXY(point.getX(), point.getY());
}
try {
return builder.build();
} catch (IllegalArgumentException e) {
// This is what happens when we create a shape that is invalid. Although it is conceivable that there are cases where
// the exception is thrown incorrectly, we aren't going to be able to do that in this random test.
continue;
}
}
}
@Override
protected Point randomPointInEmptyShape(Shape shape) {
throw new IllegalStateException("unexpected; need to finish test code");
}
@Override
protected int getWithinMinimum(int laps) {
// Long/thin so lets just find 1.
return 1;
}
}.testRelateWithRectangle();
}
@Test
public void testGeoPathRect() {
new AbstractRectIntersectionTestHelper(ctx) {
@Override
protected Shape generateRandomShape(Point nearP) {
final Point centerPoint = randomPoint();
final int maxDistance = random().nextInt(maxRadius -20) + 20;
final Circle pointZone = ctx.getShapeFactory().circle(centerPoint, maxDistance);
final int pointCount = random().nextInt(5) + 1;
final double width = (random().nextInt(89)+1) * DEGREES_TO_RADIANS;
final ShapeFactory.LineStringBuilder builder = ctx.getShapeFactory().lineString();
while (true) {
for (int i = 0; i < pointCount; i++) {
final Point nextPoint = randomPointIn(pointZone);
builder.pointXY(nextPoint.getX(), nextPoint.getY());
}
builder.buffer(width);
try {
return builder.build();
} catch (IllegalArgumentException e) {
// This is what happens when we create a shape that is invalid. Although it is conceivable that there are cases where
// the exception is thrown incorrectly, we aren't going to be able to do that in this random test.
continue;
}
}
}
@Override
protected Point randomPointInEmptyShape(Shape shape) {
throw new IllegalStateException("unexpected; need to finish test code");
}
@Override
protected int getWithinMinimum(int laps) {
// Long/thin so lets just find 1.
return 1;
}
}.testRelateWithRectangle();
}
}