mirror of https://github.com/apache/lucene.git
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:
parent
4cbf0da8c6
commit
69e5f9bdf4
|
@ -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)
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
2b8019b6249bb05d81d3a3094e468753e2b21311
|
|
@ -1 +0,0 @@
|
|||
4234d12b1ba4d4b539fb3e29edd948a99539d9eb
|
|
@ -0,0 +1 @@
|
|||
bdcdf20a723516a233b5bcc0ca7d4decaa88b6ed
|
|
@ -0,0 +1 @@
|
|||
6e16edaf6b1ba76db7f08c2f3723fce3b358ecc3
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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. */
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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; }
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
6e16edaf6b1ba76db7f08c2f3723fce3b358ecc3
|
Loading…
Reference in New Issue