blob: 0c18f5d7ddea10b6cd2ec45f7e5664029b6b7180 [file] [log] [blame]
/*
* 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.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.Rectangle;
import org.locationtech.spatial4j.shape.Shape;
import org.locationtech.spatial4j.shape.SpatialRelation;
import org.locationtech.spatial4j.shape.impl.Range;
import static org.locationtech.spatial4j.shape.SpatialRelation.CONTAINS;
import static org.locationtech.spatial4j.shape.SpatialRelation.WITHIN;
import org.apache.lucene.util.LuceneTestCase;
import static com.carrotsearch.randomizedtesting.RandomizedTest.*;
/**
* A base test class with utility methods to help test shapes.
* Extends from RandomizedTest.
*/
public abstract class RandomizedShapeTestCase extends LuceneTestCase {
protected static final double EPS = 10e-9;
protected SpatialContext ctx;//needs to be set ASAP
/** Used to reduce the space of numbers to increase the likelihood that
* random numbers become equivalent, and thus trigger different code paths.
* Also makes some random shapes easier to manually examine.
*/
protected final double DIVISIBLE = 2;// even coordinates; (not always used)
protected RandomizedShapeTestCase() {
}
public RandomizedShapeTestCase(SpatialContext ctx) {
this.ctx = ctx;
}
@SuppressWarnings("unchecked")
public static void checkShapesImplementEquals( Class<?>[] classes ) {
for( Class<?> clazz : classes ) {
try {
clazz.getDeclaredMethod( "equals", Object.class );
} catch (Exception e) {
fail("Shape needs to define 'equals' : " + clazz.getName());
}
try {
clazz.getDeclaredMethod( "hashCode" );
} catch (Exception e) {
fail("Shape needs to define 'hashCode' : " + clazz.getName());
}
}
}
//These few norm methods normalize the arguments for creating a shape to
// account for the dateline. Some tests loop past the dateline or have offsets
// that go past it and it's easier to have them coded that way and correct for
// it here. These norm methods should be used when needed, not frivolously.
protected double normX(double x) {
return ctx.isGeo() ? DistanceUtils.normLonDEG(x) : x;
}
protected double normY(double y) {
return ctx.isGeo() ? DistanceUtils.normLatDEG(y) : y;
}
protected Rectangle makeNormRect(double minX, double maxX, double minY, double maxY) {
if (ctx.isGeo()) {
if (Math.abs(maxX - minX) >= 360) {
minX = -180;
maxX = 180;
} else {
minX = DistanceUtils.normLonDEG(minX);
maxX = DistanceUtils.normLonDEG(maxX);
}
} else {
if (maxX < minX) {
double t = minX;
minX = maxX;
maxX = t;
}
minX = boundX(minX, ctx.getWorldBounds());
maxX = boundX(maxX, ctx.getWorldBounds());
}
if (maxY < minY) {
double t = minY;
minY = maxY;
maxY = t;
}
minY = boundY(minY, ctx.getWorldBounds());
maxY = boundY(maxY, ctx.getWorldBounds());
return ctx.makeRectangle(minX, maxX, minY, maxY);
}
public static double divisible(double v, double divisible) {
return (int) (Math.round(v / divisible) * divisible);
}
protected double divisible(double v) {
return divisible(v, DIVISIBLE);
}
/** reset()'s p, and confines to world bounds. Might not be divisible if
* the world bound isn't divisible too.
*/
protected Point divisible(Point p) {
Rectangle bounds = ctx.getWorldBounds();
double newX = boundX( divisible(p.getX()), bounds );
double newY = boundY( divisible(p.getY()), bounds );
p.reset(newX, newY);
return p;
}
static double boundX(double i, Rectangle bounds) {
return bound(i, bounds.getMinX(), bounds.getMaxX());
}
static double boundY(double i, Rectangle bounds) {
return bound(i, bounds.getMinY(), bounds.getMaxY());
}
static double bound(double i, double min, double max) {
if (i < min) return min;
if (i > max) return max;
return i;
}
protected void assertRelation(SpatialRelation expected, Shape a, Shape b) {
assertRelation(null, expected, a, b);
}
protected void assertRelation(String msg, SpatialRelation expected, Shape a, Shape b) {
_assertIntersect(msg, expected, a, b);
//check flipped a & b w/ transpose(), while we're at it
_assertIntersect(msg, expected.transpose(), b, a);
}
private void _assertIntersect(String msg, SpatialRelation expected, Shape a, Shape b) {
SpatialRelation sect = a.relate(b);
if (sect == expected)
return;
msg = ((msg == null) ? "" : msg+"\r") + a +" intersect "+b;
if (expected == WITHIN || expected == CONTAINS) {
if (a.getClass().equals(b.getClass())) // they are the same shape type
assertEquals(msg,a,b);
else {
//they are effectively points or lines that are the same location
assertTrue(msg,!a.hasArea());
assertTrue(msg,!b.hasArea());
Rectangle aBBox = a.getBoundingBox();
Rectangle bBBox = b.getBoundingBox();
if (aBBox.getHeight() == 0 && bBBox.getHeight() == 0
&& (aBBox.getMaxY() == 90 && bBBox.getMaxY() == 90
|| aBBox.getMinY() == -90 && bBBox.getMinY() == -90))
;//== a point at the pole
else
assertEquals(msg, aBBox, bBBox);
}
} else {
assertEquals(msg,expected,sect);//always fails
}
}
protected void assertEqualsRatio(String msg, double expected, double actual) {
double delta = Math.abs(actual - expected);
double base = Math.min(actual, expected);
double deltaRatio = base==0 ? delta : Math.min(delta,delta / base);
assertEquals(msg,0,deltaRatio, EPS);
}
protected int randomIntBetweenDivisible(int start, int end) {
return randomIntBetweenDivisible(start, end, (int)DIVISIBLE);
}
/** Returns a random integer between [start, end]. Integers between must be divisible by the 3rd argument. */
protected int randomIntBetweenDivisible(int start, int end, int divisible) {
// DWS: I tested this
int divisStart = (int) Math.ceil( (start+1) / (double)divisible );
int divisEnd = (int) Math.floor( (end-1) / (double)divisible );
int divisRange = Math.max(0,divisEnd - divisStart + 1);
int r = randomInt(1 + divisRange);//remember that '0' is counted
if (r == 0)
return start;
if (r == 1)
return end;
return (r-2 + divisStart)*divisible;
}
protected Rectangle randomRectangle(Point nearP) {
Rectangle bounds = ctx.getWorldBounds();
if (nearP == null)
nearP = randomPointIn(bounds);
Range xRange = randomRange(rarely() ? 0 : nearP.getX(), Range.xRange(bounds, ctx));
Range yRange = randomRange(rarely() ? 0 : nearP.getY(), Range.yRange(bounds, ctx));
return makeNormRect(
divisible(xRange.getMin()),
divisible(xRange.getMax()),
divisible(yRange.getMin()),
divisible(yRange.getMax()) );
}
private Range randomRange(double near, Range bounds) {
double mid = near + randomGaussian() * bounds.getWidth() / 6;
double width = Math.abs(randomGaussian()) * bounds.getWidth() / 6;//1/3rd
return new Range(mid - width / 2, mid + width / 2);
}
private double randomGaussianZeroTo(double max) {
if (max == 0)
return max;
assert max > 0;
double r;
do {
r = Math.abs(randomGaussian()) * (max * 0.50);
} while (r > max);
return r;
}
protected Rectangle randomRectangle(int divisible) {
double rX = randomIntBetweenDivisible(-180, 180, divisible);
double rW = randomIntBetweenDivisible(0, 360, divisible);
double rY1 = randomIntBetweenDivisible(-90, 90, divisible);
double rY2 = randomIntBetweenDivisible(-90, 90, divisible);
double rYmin = Math.min(rY1,rY2);
double rYmax = Math.max(rY1,rY2);
if (rW > 0 && rX == 180)
rX = -180;
return makeNormRect(rX, rX + rW, rYmin, rYmax);
}
protected Point randomPoint() {
return randomPointIn(ctx.getWorldBounds());
}
protected Point randomPointIn(Circle c) {
double d = c.getRadius() * randomDouble();
double angleDEG = 360 * randomDouble();
Point p = ctx.getDistCalc().pointOnBearing(c.getCenter(), d, angleDEG, ctx, null);
assertEquals(CONTAINS,c.relate(p));
return p;
}
protected Point randomPointIn(Rectangle r) {
double x = r.getMinX() + randomDouble()*r.getWidth();
double y = r.getMinY() + randomDouble()*r.getHeight();
x = normX(x);
y = normY(y);
Point p = ctx.makePoint(x,y);
assertEquals(CONTAINS,r.relate(p));
return p;
}
protected Point randomPointInOrNull(Shape shape) {
if (!shape.hasArea())// or try the center?
throw new UnsupportedOperationException("Need area to define shape!");
Rectangle bbox = shape.getBoundingBox();
for (int i = 0; i < 1000; i++) {
Point p = randomPointIn(bbox);
if (shape.relate(p).intersects()) {
return p;
}
}
return null;//tried too many times and failed
}
}