LUCENE-6810: Spatial4j 0.5 upgrade. Mostly fixes a few edge-case bugs.

* the spatial4j tests jar is published and we use some utilities there; this adds a test dependency on it & SLF4J.

git-svn-id: https://svn.apache.org/repos/asf/lucene/dev/trunk@1704759 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
David Wayne Smiley 2015-09-23 02:46:43 +00:00
parent 4cbf0da8c6
commit 69e5f9bdf4
15 changed files with 68 additions and 360 deletions

View File

@ -150,6 +150,10 @@ Bug Fixes
handling a tragic exception but another is still committing (Mike
McCandless)
* LUCENE-6810: Upgrade to Spatial4j 0.5 -- fixes some edge-case bugs in the
spatial module. See https://github.com/locationtech/spatial4j/blob/master/CHANGES.md
(David Smiley)
Other
* LUCENE-6812: Upgrade RandomizedTesting to 2.1.17. (Dawid Weiss)

View File

@ -41,7 +41,7 @@ com.google.inject.guice.version = 3.0
/com.googlecode.mp4parser/isoparser = 1.0.2
/com.ibm.icu/icu4j = 54.1
/com.pff/java-libpst = 0.8.1
/com.spatial4j/spatial4j = 0.4.1
/com.spatial4j/spatial4j = 0.5
com.sun.jersey.version = 1.9
/com.sun.jersey.contribs/jersey-guice = ${com.sun.jersey.version}

View File

@ -0,0 +1 @@
2b8019b6249bb05d81d3a3094e468753e2b21311

View File

@ -1 +0,0 @@
4234d12b1ba4d4b539fb3e29edd948a99539d9eb

View File

@ -0,0 +1 @@
bdcdf20a723516a233b5bcc0ca7d4decaa88b6ed

View File

@ -0,0 +1 @@
6e16edaf6b1ba76db7f08c2f3723fce3b358ecc3

View File

@ -16,13 +16,20 @@
specific language governing permissions and limitations
under the License.
-->
<ivy-module version="2.0">
<ivy-module version="2.0" xmlns:maven="http://ant.apache.org/ivy/maven">
<info organisation="org.apache.lucene" module="spatial"/>
<configurations defaultconfmapping="compile->master">
<configurations defaultconfmapping="compile->master;test->master">
<conf name="compile" transitive="false"/>
<conf name="test" transitive="false"/>
</configurations>
<dependencies>
<dependency org="com.spatial4j" name="spatial4j" rev="${/com.spatial4j/spatial4j}" conf="compile"/>
<exclude org="*" ext="*" matcher="regexp" type="${ivy.exclude.types}"/>
<dependency org="com.spatial4j" name="spatial4j" rev="${/com.spatial4j/spatial4j}" conf="compile">
<artifact name="spatial4j" ext="jar" />
<artifact name="spatial4j" type="test" ext="jar" maven:classifier="tests" />
</dependency>
<dependency org="org.slf4j" name="slf4j-api" rev="${/org.slf4j/slf4j-api}" conf="test"/>
<exclude org="*" ext="*" matcher="regexp" type="${ivy.exclude.types}"/>
</dependencies>
</ivy-module>

View File

@ -266,6 +266,11 @@ public abstract class NumberRangePrefixTree extends SpatialPrefixTree {
lastLevelInCommon = level - 1;
}
@Override
public SpatialContext getContext() {
return DUMMY_CTX;
}
public UnitNRShape getMinUnit() { return minLV; }
public UnitNRShape getMaxUnit() { return maxLV; }
@ -953,6 +958,11 @@ public abstract class NumberRangePrefixTree extends SpatialPrefixTree {
return answer;
}
@Override
public SpatialContext getContext() {
return DUMMY_CTX;
}
@Override
public int hashCode() {
//trick to re-use bytesref; provided that we re-instate it

View File

@ -61,6 +61,11 @@ public class Geo3dShape implements Shape {
this.shape = shape;
}
@Override
public SpatialContext getContext() {
return ctx;
}
@Override
public SpatialRelation relate(Shape other) {
if (other instanceof Rectangle)

View File

@ -57,27 +57,18 @@ public class TestBBoxStrategy extends RandomSpatialOpStrategyTestCase {
int worldHeight = (int) Math.round(world.getHeight());
int deltaTop = nextIntInclusive(worldHeight);
int deltaBottom = nextIntInclusive(worldHeight - deltaTop);
double rectMinX = world.getMinX() + deltaLeft;
double rectMaxX = world.getMaxX() - deltaRight;
if (ctx.isGeo()) {
int shift = 0;
if ((deltaLeft != 0 || deltaRight != 0)) {
//if geo & doesn't world-wrap, we shift randomly to potentially cross dateline
shift = nextIntInclusive(360);
}
rectMinX = DistanceUtils.normLonDEG(rectMinX + shift);
rectMaxX = DistanceUtils.normLonDEG(rectMaxX + shift);
if (rectMinX == 180 && rectMaxX == 180) {
// Work-around for https://github.com/spatial4j/spatial4j/issues/85
rectMinX = -180;
rectMaxX = -180;
}
if (ctx.isGeo() && (deltaLeft != 0 || deltaRight != 0)) {
//if geo & doesn't world-wrap, we shift randomly to potentially cross dateline
int shift = nextIntInclusive(360);
return ctx.makeRectangle(
DistanceUtils.normLonDEG(world.getMinX() + deltaLeft + shift),
DistanceUtils.normLonDEG(world.getMaxX() - deltaRight + shift),
world.getMinY() + deltaBottom, world.getMaxY() - deltaTop);
} else {
return ctx.makeRectangle(
world.getMinX() + deltaLeft, world.getMaxX() - deltaRight,
world.getMinY() + deltaBottom, world.getMaxY() - deltaTop);
}
return ctx.makeRectangle(
rectMinX,
rectMaxX,
world.getMinY() + deltaBottom, world.getMaxY() - deltaTop);
}
/** next int, inclusive, rounds to multiple of 10 if given evenly divisible. */

View File

@ -447,7 +447,7 @@ public class RandomSpatialOpFuzzyPrefixTreeTest extends StrategyTestCase {
final boolean biasContainsThenWithin;
public ShapePair(Shape shape1, Shape shape2, boolean containsThenWithin) {
super(Arrays.asList(shape1, shape2), ctx);
super(Arrays.asList(shape1, shape2), RandomSpatialOpFuzzyPrefixTreeTest.this.ctx);
this.shape1 = shape1;
this.shape2 = shape2;
this.shape1_2D = toNonGeo(shape1);

View File

@ -22,10 +22,12 @@ import java.util.List;
import java.util.Random;
import com.carrotsearch.randomizedtesting.RandomizedContext;
import com.spatial4j.core.TestLog;
import com.spatial4j.core.context.SpatialContext;
import com.spatial4j.core.distance.DistanceUtils;
import com.spatial4j.core.shape.Circle;
import com.spatial4j.core.shape.Point;
import com.spatial4j.core.shape.RectIntersectionTestHelper;
import org.apache.lucene.geo3d.LatLonBounds;
import org.apache.lucene.geo3d.GeoBBox;
import org.apache.lucene.geo3d.GeoBBoxFactory;
@ -44,7 +46,7 @@ public abstract class Geo3dShapeRectRelationTestCase extends RandomizedShapeTest
protected final static double RADIANS_PER_DEGREE = Math.PI/180.0;
@Rule
public final LogRule testLog = LogRule.instance;
public final TestLog testLog = TestLog.instance;
protected static Random random() {
return RandomizedContext.current().getRandom();
@ -91,15 +93,26 @@ public abstract class Geo3dShapeRectRelationTestCase extends RandomizedShapeTest
super(ctx);
}
@Override
protected int getMaxLaps() {
//sometimes, getWithinMinimum needs some more attempts then normal; 20k is suggested max.
return 200_000;//200k
//20 times each -- should be plenty
protected int getContainsMinimum(int laps) {
return 20;
}
@Override
protected int getDefaultMinimumPredicateFrequency(int maxLaps) {
return 20;//20 times each -- should be plenty in 200k
protected int getIntersectsMinimum(int laps) {
return 20;
}
protected int getWithinMinimum(int laps) {
return 20;
}
protected int getDisjointMinimum(int laps) {
return 20;
}
protected int getBoundingMinimum(int laps) {
return 20;
}
}

View File

@ -1,83 +0,0 @@
package org.apache.lucene.spatial.spatial4j;
/*
* 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.
*/
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import com.carrotsearch.randomizedtesting.rules.TestRuleAdapter;
/**
* A utility logger for tests in which log statements are logged following
* test failure only. Add this to a JUnit based test class with a {@link org.junit.Rule}
* annotation.
*/
public class LogRule extends TestRuleAdapter {
//TODO does this need to be threadsafe (such as via thread-local state)?
private static ArrayList<LogEntry> logStack = new ArrayList<LogEntry>();
private static final int MAX_LOGS = 1000;
public static final LogRule instance = new LogRule();
private LogRule() {}
@Override
protected void before() throws Throwable {
logStack.clear();
}
@Override
protected void afterAlways(List<Throwable> errors) throws Throwable {
if (!errors.isEmpty())
logThenClear();
}
private void logThenClear() {
for (LogEntry entry : logStack) {
//no SLF4J in Lucene... fallback to this
if (entry.args != null && entry.args.length > 0) {
System.out.println(entry.msg + " " + Arrays.asList(entry.args) + "(no slf4j subst; sorry)");
} else {
System.out.println(entry.msg);
}
}
logStack.clear();
}
public static void clear() {
logStack.clear();
}
/**
* Enqueues a log message with substitution arguments ala SLF4J (i.e. {} syntax).
* If the test fails then it'll be logged then, otherwise it'll be forgotten.
*/
public static void log(String msg, Object... args) {
if (logStack.size() > MAX_LOGS) {
throw new RuntimeException("Too many log statements: "+logStack.size() + " > "+MAX_LOGS);
}
LogEntry entry = new LogEntry();
entry.msg = msg;
entry.args = args;
logStack.add(entry);
}
private static class LogEntry { String msg; Object[] args; }
}

View File

@ -1,242 +0,0 @@
package org.apache.lucene.spatial.spatial4j;
/*
* 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.
*/
import com.spatial4j.core.context.SpatialContext;
import com.spatial4j.core.shape.Point;
import com.spatial4j.core.shape.Rectangle;
import com.spatial4j.core.shape.Shape;
import com.spatial4j.core.shape.SpatialRelation;
import com.spatial4j.core.shape.impl.InfBufLine;
import com.spatial4j.core.shape.impl.PointImpl;
import static com.spatial4j.core.shape.SpatialRelation.CONTAINS;
import static com.spatial4j.core.shape.SpatialRelation.DISJOINT;
public abstract class RectIntersectionTestHelper<S extends Shape> extends RandomizedShapeTestCase {
public RectIntersectionTestHelper(SpatialContext ctx) {
super(ctx);
}
/** Override to return true if generateRandomShape is essentially a Rectangle. */
protected boolean isRandomShapeRectangular() {
return false;
}
protected abstract S generateRandomShape(Point nearP);
/** shape has no area; return a point in it */
protected abstract Point randomPointInEmptyShape(S shape);
// Minimum distribution of relationships
// Each shape has different characteristics, so we don't expect (for instance) shapes that
// are likely to be long and thin to contain as many rectangles as those that
// short and fat.
/** Called once by {@link #testRelateWithRectangle()} to determine the max laps to try before failing. */
protected int getMaxLaps() {
return scaledRandomIntBetween(20_000, 200_000);
}
/** The minimum number of times we need to see each predicate in {@code laps} iterations. */
protected int getDefaultMinimumPredicateFrequency(int maxLaps) {
return maxLaps / 1000;
}
protected int getContainsMinimum(int maxLaps) {
return getDefaultMinimumPredicateFrequency(maxLaps);
}
protected int getIntersectsMinimum(int maxLaps) {
return getDefaultMinimumPredicateFrequency(maxLaps);
}
protected int getWithinMinimum(int maxLaps) {
return getDefaultMinimumPredicateFrequency(maxLaps);
}
protected int getDisjointMinimum(int maxLaps) {
return getDefaultMinimumPredicateFrequency(maxLaps);
}
protected int getBoundingMinimum(int maxLaps) {
return getDefaultMinimumPredicateFrequency(maxLaps);
}
@SuppressWarnings("unchecked")
@Override
protected Point randomPointInOrNull(Shape shape) {
if (!shape.hasArea()) {
final Point pt = randomPointInEmptyShape((S) shape);
assert shape.relate(pt).intersects() : "faulty randomPointInEmptyShape";
return pt;
}
return super.randomPointInOrNull(shape);
}
public void testRelateWithRectangle() {
//counters for the different intersection cases
int i_C = 0, i_I = 0, i_W = 0, i_D = 0, i_bboxD = 0;
int lap = 0;
final int MAXLAPS = getMaxLaps();
while(i_C < getContainsMinimum(MAXLAPS) || i_I < getIntersectsMinimum(MAXLAPS) || i_W < getWithinMinimum(MAXLAPS)
|| (!isRandomShapeRectangular() && i_D < getDisjointMinimum(MAXLAPS)) || i_bboxD < getBoundingMinimum(MAXLAPS)) {
lap++;
LogRule.clear();
if (lap > MAXLAPS) {
fail("Did not find enough contains/within/intersection/disjoint/bounds cases in a reasonable number" +
" of attempts. CWIDbD: " +
i_C + "("+getContainsMinimum(MAXLAPS)+")," +
i_W + "("+getWithinMinimum(MAXLAPS)+")," +
i_I + "("+getIntersectsMinimum(MAXLAPS)+")," +
i_D + "("+getDisjointMinimum(MAXLAPS)+")," +
i_bboxD + "("+getBoundingMinimum(MAXLAPS)+")"
+ " Laps exceeded " + MAXLAPS);
}
Point nearP = randomPointIn(ctx.getWorldBounds());
S s = generateRandomShape(nearP);
Rectangle r = randomRectangle(s.getBoundingBox().getCenter());
SpatialRelation ic = s.relate(r);
LogRule.log("S-R Rel: {}, Shape {}, Rectangle {} lap# {}", ic, s, r, lap);
if (ic != DISJOINT) {
assertTrue("if not disjoint then the shape's bbox shouldn't be disjoint",
s.getBoundingBox().relate(r).intersects());
}
try {
int MAX_TRIES = scaledRandomIntBetween(10, 100);
switch (ic) {
case CONTAINS:
i_C++;
for (int j = 0; j < MAX_TRIES; j++) {
Point p = randomPointIn(r);
assertRelation(null, CONTAINS, s, p);
}
break;
case WITHIN:
i_W++;
for (int j = 0; j < MAX_TRIES; j++) {
Point p = randomPointInOrNull(s);
if (p == null) {//couldn't find a random point in shape
break;
}
assertRelation(null, CONTAINS, r, p);
}
break;
case DISJOINT:
if (!s.getBoundingBox().relate(r).intersects()) {//bboxes are disjoint
i_bboxD++;
if (i_bboxD >= getBoundingMinimum(MAXLAPS))
break;
} else {
i_D++;
}
for (int j = 0; j < MAX_TRIES; j++) {
Point p = randomPointIn(r);
assertRelation(null, DISJOINT, s, p);
}
break;
case INTERSECTS:
i_I++;
SpatialRelation pointR = null;//set once
Rectangle randomPointSpace = null;
MAX_TRIES = 1000;//give many attempts
for (int j = 0; j < MAX_TRIES; j++) {
Point p;
if (j < 4) {
p = new PointImpl(0, 0, ctx);
InfBufLine.cornerByQuadrant(r, j + 1, p);
} else {
if (randomPointSpace == null) {
if (pointR == DISJOINT) {
randomPointSpace = intersectRects(r,s.getBoundingBox());
} else {//CONTAINS
randomPointSpace = r;
}
}
p = randomPointIn(randomPointSpace);
}
SpatialRelation pointRNew = s.relate(p);
if (pointR == null) {
pointR = pointRNew;
} else if (pointR != pointRNew) {
break;
} else if (j >= MAX_TRIES) {
//TODO consider logging instead of failing
fail("Tried intersection brute-force too many times without success");
}
}
break;
default: fail(""+ic);
} // switch
} catch (AssertionError e) {
onAssertFail(e, s, r, ic);
}
} // while loop
System.out.println("Laps: "+lap + " CWIDbD: "+i_C+","+i_W+","+i_I+","+i_D+","+i_bboxD);
}
protected void onAssertFail(AssertionError e, S s, Rectangle r, SpatialRelation ic) {
throw e;
}
private Rectangle intersectRects(Rectangle r1, Rectangle r2) {
assert r1.relate(r2).intersects();
final double minX, maxX;
if (r1.relateXRange(r2.getMinX(),r2.getMinX()).intersects()) {
minX = r2.getMinX();
} else {
minX = r1.getMinX();
}
if (r1.relateXRange(r2.getMaxX(),r2.getMaxX()).intersects()) {
maxX = r2.getMaxX();
} else {
maxX = r1.getMaxX();
}
final double minY, maxY;
if (r1.relateYRange(r2.getMinY(),r2.getMinY()).intersects()) {
minY = r2.getMinY();
} else {
minY = r1.getMinY();
}
if (r1.relateYRange(r2.getMaxY(),r2.getMaxY()).intersects()) {
maxY = r2.getMaxY();
} else {
maxY = r1.getMaxY();
}
return ctx.makeRectangle(minX, maxX, minY, maxY);
}
}

View File

@ -0,0 +1 @@
6e16edaf6b1ba76db7f08c2f3723fce3b358ecc3