/* * 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 } }