diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt index 5d31396b52c..c08e215ac98 100644 --- a/lucene/CHANGES.txt +++ b/lucene/CHANGES.txt @@ -72,6 +72,10 @@ Improvements disk. This change adds an expert setting to opt ouf of this behavior unless 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 ======================= API Changes diff --git a/lucene/spatial-extras/src/java/org/apache/lucene/spatial/spatial4j/Geo3dCircleShape.java b/lucene/spatial-extras/src/java/org/apache/lucene/spatial/spatial4j/Geo3dCircleShape.java index ccef92a4bc9..d01e2b8e74d 100644 --- a/lucene/spatial-extras/src/java/org/apache/lucene/spatial/spatial4j/Geo3dCircleShape.java +++ b/lucene/spatial-extras/src/java/org/apache/lucene/spatial/spatial4j/Geo3dCircleShape.java @@ -20,13 +20,10 @@ package org.apache.lucene.spatial.spatial4j; import org.apache.lucene.spatial3d.geom.GeoCircle; import org.apache.lucene.spatial3d.geom.GeoCircleFactory; import org.apache.lucene.spatial3d.geom.GeoPointShapeFactory; -import org.apache.lucene.spatial3d.geom.PlanetModel; 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.Shape; -import org.locationtech.spatial4j.shape.SpatialRelation; /** * Specialization of a {@link Geo3dShape} which represents a {@link Circle}. @@ -67,16 +64,4 @@ public class Geo3dCircleShape extends Geo3dShape implements Circle { } 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); - } } diff --git a/lucene/spatial-extras/src/java/org/apache/lucene/spatial/spatial4j/Geo3dDistanceCalculator.java b/lucene/spatial-extras/src/java/org/apache/lucene/spatial/spatial4j/Geo3dDistanceCalculator.java index 5154de431d4..8fdb4813dd5 100644 --- a/lucene/spatial-extras/src/java/org/apache/lucene/spatial/spatial4j/Geo3dDistanceCalculator.java +++ b/lucene/spatial-extras/src/java/org/apache/lucene/spatial/spatial4j/Geo3dDistanceCalculator.java @@ -73,62 +73,20 @@ public class Geo3dDistanceCalculator implements DistanceCalculator { @Override 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; GeoPoint point = (GeoPoint) geoFrom.shape; - double lat = point.getLatitude(); - double lon = point.getLongitude(); double dist = DistanceUtils.DEGREES_TO_RADIANS * distDEG; double bearing = DistanceUtils.DEGREES_TO_RADIANS * bearingDEG; - - double sinα1 = Math.sin(bearing); - double cosα1 = Math.cos(bearing); - - double tanU1 = (1 - planetModel.flattening) * Math.tan(lat); - double cosU1 = 1 / Math.sqrt((1 + tanU1 * tanU1)); - double sinU1 = tanU1 * cosU1; - - double σ1 = Math.atan2(tanU1, cosα1); - 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"); + GeoPoint newPoint = planetModel.surfacePointOnBearing(point, dist, bearing); + double newLat = newPoint.getLatitude() * DistanceUtils.RADIANS_TO_DEGREES; + double newLon = newPoint.getLongitude() * DistanceUtils.RADIANS_TO_DEGREES; + if (reuse != null) { + reuse.reset(newLon, newLat); + return reuse; + } + else { + return ctx.getShapeFactory().pointXY(newLon, newLat); } - - 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 diff --git a/lucene/spatial-extras/src/java/org/apache/lucene/spatial/spatial4j/Geo3dShape.java b/lucene/spatial-extras/src/java/org/apache/lucene/spatial/spatial4j/Geo3dShape.java index eedf7d6fc52..327ac8f581f 100644 --- a/lucene/spatial-extras/src/java/org/apache/lucene/spatial/spatial4j/Geo3dShape.java +++ b/lucene/spatial-extras/src/java/org/apache/lucene/spatial/spatial4j/Geo3dShape.java @@ -112,11 +112,7 @@ public class Geo3dShape implements Shape { if (bbox == null) { LatLonBounds bounds = new LatLonBounds(); shape.getBounds(bounds); - double leftLon = bounds.checkNoLongitudeBound() ? -Math.PI : bounds.getLeftLongitude(); - 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); + GeoBBox geoBBox = GeoBBoxFactory.makeGeoBBox(shape.getPlanetModel(), bounds); bbox = new Geo3dRectangleShape(geoBBox, spatialcontext); this.boundingBox = bbox; } diff --git a/lucene/spatial-extras/src/java/org/apache/lucene/spatial/spatial4j/Geo3dShapeFactory.java b/lucene/spatial-extras/src/java/org/apache/lucene/spatial/spatial4j/Geo3dShapeFactory.java index a80a0439b29..282d93be427 100644 --- a/lucene/spatial-extras/src/java/org/apache/lucene/spatial/spatial4j/Geo3dShapeFactory.java +++ b/lucene/spatial-extras/src/java/org/apache/lucene/spatial/spatial4j/Geo3dShapeFactory.java @@ -55,6 +55,13 @@ public class Geo3dShapeFactory implements ShapeFactory { private SpatialContext context; 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") public Geo3dShapeFactory(SpatialContext context, SpatialContextFactory factory) { this.context = context; @@ -67,6 +74,16 @@ public class Geo3dShapeFactory implements ShapeFactory { 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 public boolean isNormWrapLongitude() { return normWrapLongitude; @@ -150,10 +167,23 @@ public class Geo3dShapeFactory implements ShapeFactory { @Override public Circle circle(double x, double y, double distance) { - GeoCircle circle = GeoCircleFactory.makeGeoCircle(planetModel, - y * DistanceUtils.DEGREES_TO_RADIANS, - x * DistanceUtils.DEGREES_TO_RADIANS, - distance * DistanceUtils.DEGREES_TO_RADIANS); + GeoCircle circle; + if (planetModel.isSphere()) { + circle = GeoCircleFactory.makeGeoCircle(planetModel, + y * DistanceUtils.DEGREES_TO_RADIANS, + x * 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); } @@ -238,8 +268,7 @@ public class Geo3dShapeFactory implements ShapeFactory { /** * Geo3d implementation of {@link org.locationtech.spatial4j.shape.ShapeFactory.LineStringBuilder} to generate - * nine Strings. Note that GeoPath needs a buffer so we set the - * buffer to 1e-10. + * line strings. */ private class Geo3dLineStringBuilder extends Geo3dPointBuilder implements LineStringBuilder { @@ -373,7 +402,7 @@ public class Geo3dShapeFactory implements ShapeFactory { /** * Geo3d implementation of {@link org.locationtech.spatial4j.shape.ShapeFactory.MultiShapeBuilder} to generate - * geometry collections + * geometry collections. * * @param is the type of shapes. */ diff --git a/lucene/spatial-extras/src/test/org/apache/lucene/spatial/spatial4j/Geo3dShapeRectRelationTestCase.java b/lucene/spatial-extras/src/test/org/apache/lucene/spatial/spatial4j/Geo3dShapeRectRelationTestCase.java deleted file mode 100644 index 9873012f6b0..00000000000 --- a/lucene/spatial-extras/src/test/org/apache/lucene/spatial/spatial4j/Geo3dShapeRectRelationTestCase.java +++ /dev/null @@ -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 { - - 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 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); - } -} diff --git a/lucene/spatial-extras/src/test/org/apache/lucene/spatial/spatial4j/Geo3dShapeSphereModelRectRelationTest.java b/lucene/spatial-extras/src/test/org/apache/lucene/spatial/spatial4j/Geo3dShapeSphereModelRectRelationTest.java index 20db21c61c3..bf152b7fdfb 100644 --- a/lucene/spatial-extras/src/test/org/apache/lucene/spatial/spatial4j/Geo3dShapeSphereModelRectRelationTest.java +++ b/lucene/spatial-extras/src/test/org/apache/lucene/spatial/spatial4j/Geo3dShapeSphereModelRectRelationTest.java @@ -34,13 +34,13 @@ import org.locationtech.spatial4j.shape.Point; import org.locationtech.spatial4j.shape.Rectangle; import org.locationtech.spatial4j.shape.SpatialRelation; -public class Geo3dShapeSphereModelRectRelationTest extends Geo3dShapeRectRelationTestCase { +public class Geo3dShapeSphereModelRectRelationTest extends ShapeRectRelationTestCase { + + PlanetModel planetModel = PlanetModel.SPHERE; public Geo3dShapeSphereModelRectRelationTest() { - super(PlanetModel.SPHERE); Geo3dSpatialContextFactory factory = new Geo3dSpatialContextFactory(); - factory.planetModel = PlanetModel.SPHERE; - //factory.distCalc = new GeodesicSphereDistCalc.Haversine(); + factory.planetModel = planetModel; this.ctx = factory.newSpatialContext(); } diff --git a/lucene/spatial-extras/src/test/org/apache/lucene/spatial/spatial4j/Geo3dShapeWGS84ModelRectRelationTest.java b/lucene/spatial-extras/src/test/org/apache/lucene/spatial/spatial4j/Geo3dShapeWGS84ModelRectRelationTest.java index 22d7bd4cbdb..5a7b4b52f48 100644 --- a/lucene/spatial-extras/src/test/org/apache/lucene/spatial/spatial4j/Geo3dShapeWGS84ModelRectRelationTest.java +++ b/lucene/spatial-extras/src/test/org/apache/lucene/spatial/spatial4j/Geo3dShapeWGS84ModelRectRelationTest.java @@ -30,14 +30,16 @@ import org.locationtech.spatial4j.shape.Circle; import org.locationtech.spatial4j.shape.Point; import org.locationtech.spatial4j.shape.SpatialRelation; -public class Geo3dShapeWGS84ModelRectRelationTest extends Geo3dShapeRectRelationTestCase { +public class Geo3dShapeWGS84ModelRectRelationTest extends ShapeRectRelationTestCase { + + PlanetModel planetModel = PlanetModel.WGS84; public Geo3dShapeWGS84ModelRectRelationTest() { - super(PlanetModel.WGS84); Geo3dSpatialContextFactory factory = new Geo3dSpatialContextFactory(); - factory.planetModel = PlanetModel.WGS84; - //factory.distCalc = new GeodesicSphereDistCalc.Haversine(); + factory.planetModel = planetModel; this.ctx = factory.newSpatialContext(); + this.maxRadius = 178; + ((Geo3dShapeFactory)ctx.getShapeFactory()).setCircleAccuracy(1e-6); } @Test diff --git a/lucene/spatial-extras/src/test/org/apache/lucene/spatial/spatial4j/ShapeRectRelationTestCase.java b/lucene/spatial-extras/src/test/org/apache/lucene/spatial/spatial4j/ShapeRectRelationTestCase.java new file mode 100644 index 00000000000..7ec2a2b4272 --- /dev/null +++ b/lucene/spatial-extras/src/test/org/apache/lucene/spatial/spatial4j/ShapeRectRelationTestCase.java @@ -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 { + + 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(); + } +}