From 9302126162b31fadee9d3f13c9f6f7f4ec3dd7f0 Mon Sep 17 00:00:00 2001 From: Nicholas Knize Date: Thu, 5 Feb 2015 17:37:38 -0600 Subject: [PATCH] [GEO] Adds randomization to geo test suite Adds RandomShapeGenerator for creating random shape types. This adds a level of randomized testing to the Geospatial logic. An initial randomized GeometryCollection test is added to the GeoShapeIntegrationTest suite for validating and verifying geo_shape filter behavior. The RandomShapeGenerator can/should be used in Unit and Integration testing to avoid biased testing. closes #9588 --- .../builders/GeometryCollectionBuilder.java | 13 + .../search/geo/GeoShapeIntegrationTests.java | 30 +- .../test/geo/RandomShapeGenerator.java | 303 ++++++++++++++++++ 3 files changed, 344 insertions(+), 2 deletions(-) create mode 100644 src/test/java/org/elasticsearch/test/geo/RandomShapeGenerator.java diff --git a/src/main/java/org/elasticsearch/common/geo/builders/GeometryCollectionBuilder.java b/src/main/java/org/elasticsearch/common/geo/builders/GeometryCollectionBuilder.java index 510211a5250..ede6bcf62eb 100644 --- a/src/main/java/org/elasticsearch/common/geo/builders/GeometryCollectionBuilder.java +++ b/src/main/java/org/elasticsearch/common/geo/builders/GeometryCollectionBuilder.java @@ -21,6 +21,7 @@ package org.elasticsearch.common.geo.builders; import com.spatial4j.core.shape.Shape; import org.elasticsearch.common.geo.XShapeCollection; +import org.elasticsearch.ElasticsearchException; import org.elasticsearch.common.xcontent.XContentBuilder; import java.io.IOException; @@ -86,6 +87,18 @@ public class GeometryCollectionBuilder extends ShapeBuilder { return this; } + public ShapeBuilder getShapeAt(int i) { + if (i >= this.shapes.size() || i < 0) { + throw new ElasticsearchException("GeometryCollection contains " + this.shapes.size() + " shapes. + " + + "No shape found at index " + i); + } + return this.shapes.get(i); + } + + public int numShapes() { + return this.shapes.size(); + } + @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); diff --git a/src/test/java/org/elasticsearch/search/geo/GeoShapeIntegrationTests.java b/src/test/java/org/elasticsearch/search/geo/GeoShapeIntegrationTests.java index fe3edc21bee..0a0882b3100 100644 --- a/src/test/java/org/elasticsearch/search/geo/GeoShapeIntegrationTests.java +++ b/src/test/java/org/elasticsearch/search/geo/GeoShapeIntegrationTests.java @@ -21,7 +21,9 @@ package org.elasticsearch.search.geo; import org.elasticsearch.action.get.GetResponse; import org.elasticsearch.action.search.SearchResponse; +import org.elasticsearch.test.geo.RandomShapeGenerator; import org.elasticsearch.common.geo.ShapeRelation; +import org.elasticsearch.common.geo.builders.GeometryCollectionBuilder; import org.elasticsearch.common.geo.builders.ShapeBuilder; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentFactory; @@ -256,7 +258,7 @@ public class GeoShapeIntegrationTests extends ElasticsearchIntegrationTest { } @Test - public void testShapeFetching_path() throws Exception { + public void testShapeFetchingPath() throws Exception { createIndex("shapes"); assertAcked(prepareCreate("test").addMapping("type", "location", "type=geo_shape")); @@ -379,7 +381,31 @@ public class GeoShapeIntegrationTests extends ElasticsearchIntegrationTest { } @Test - public void testShapeFilter_geometryCollection() throws Exception { + public void testShapeFilterWithRandomGeoCollection() throws Exception { + // Create a random geometry collection. + GeometryCollectionBuilder gcb = RandomShapeGenerator.createGeometryCollection(getRandom()); + + logger.info("Created Random GeometryCollection containing " + gcb.numShapes() + " shapes"); + + createIndex("randshapes"); + assertAcked(prepareCreate("test").addMapping("type", "location", "type=geo_shape")); + + XContentBuilder docSource = gcb.toXContent(jsonBuilder().startObject().field("location"), null).endObject(); + indexRandom(true, client().prepareIndex("test", "type", "1").setSource(docSource)); + + ensureSearchable("test"); + + ShapeBuilder filterShape = (gcb.getShapeAt(randomIntBetween(0, gcb.numShapes() - 1))); + + GeoShapeFilterBuilder filter = FilterBuilders.geoShapeFilter("location", filterShape, ShapeRelation.INTERSECTS); + SearchResponse result = client().prepareSearch("test").setQuery(QueryBuilders.matchAllQuery()) + .setPostFilter(filter).get(); + assertSearchResponse(result); + assertHitCount(result, 1); + } + + @Test + public void testShapeFilterWithDefinedGeoCollection() throws Exception { createIndex("shapes"); assertAcked(prepareCreate("test").addMapping("type", "location", "type=geo_shape")); diff --git a/src/test/java/org/elasticsearch/test/geo/RandomShapeGenerator.java b/src/test/java/org/elasticsearch/test/geo/RandomShapeGenerator.java new file mode 100644 index 00000000000..9583c673f77 --- /dev/null +++ b/src/test/java/org/elasticsearch/test/geo/RandomShapeGenerator.java @@ -0,0 +1,303 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch 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.elasticsearch.test.geo; + +import com.carrotsearch.randomizedtesting.RandomizedTest; +import com.carrotsearch.randomizedtesting.generators.RandomInts; +import com.spatial4j.core.context.jts.JtsSpatialContext; +import com.spatial4j.core.distance.DistanceUtils; +import com.spatial4j.core.exception.InvalidShapeException; +import com.spatial4j.core.shape.Point; +import com.spatial4j.core.shape.Rectangle; +import com.spatial4j.core.shape.impl.Range; +import com.vividsolutions.jts.algorithm.ConvexHull; +import com.vividsolutions.jts.geom.Coordinate; +import com.vividsolutions.jts.geom.Geometry; +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.common.geo.builders.BaseLineStringBuilder; +import org.elasticsearch.common.geo.builders.GeometryCollectionBuilder; +import org.elasticsearch.common.geo.builders.LineStringBuilder; +import org.elasticsearch.common.geo.builders.MultiLineStringBuilder; +import org.elasticsearch.common.geo.builders.MultiPointBuilder; +import org.elasticsearch.common.geo.builders.PointBuilder; +import org.elasticsearch.common.geo.builders.PointCollection; +import org.elasticsearch.common.geo.builders.PolygonBuilder; +import org.elasticsearch.common.geo.builders.ShapeBuilder; + +import java.util.Random; + +import static com.spatial4j.core.shape.SpatialRelation.CONTAINS; + +/** + * Random geoshape generation utilities for randomized Geospatial testing + */ +public class RandomShapeGenerator { + + protected static JtsSpatialContext ctx = ShapeBuilder.SPATIAL_CONTEXT; + protected static final double xDIVISIBLE = 2; + protected static boolean ST_VALIDATE = true; + + public static enum ShapeType { + POINT, MULTIPOINT, LINESTRING, MULTILINESTRING, POLYGON; + private static final ShapeType[] types = values(); + + public static ShapeType randomType(Random r) { + return types[RandomInts.randomIntBetween(r, 0, types.length - 1)]; + } + } + + public static ShapeBuilder createShapeNear(Random r, Point nearPoint) throws InvalidShapeException { + return createShape(r, nearPoint, null, null); + } + + public static ShapeBuilder createShapeNear(Random r, Point nearPoint, ShapeType st) throws InvalidShapeException { + return createShape(r, nearPoint, null, st); + } + + public static ShapeBuilder createShapeWithin(Random r, Rectangle bbox) throws InvalidShapeException { + return createShape(r, null, bbox, null); + } + + public static ShapeBuilder createShapeWithin(Random r, Rectangle bbox, ShapeType st) throws InvalidShapeException { + return createShape(r, null, bbox, st); + } + + public static GeometryCollectionBuilder createGeometryCollection(Random r) throws InvalidShapeException { + return createGeometryCollection(r, null, null, 0); + } + + public static GeometryCollectionBuilder createGeometryCollectionNear(Random r, Point nearPoint) throws InvalidShapeException { + return createGeometryCollection(r, nearPoint, null, 0); + } + + public static GeometryCollectionBuilder createGeometryCollectionNear(Random r, Point nearPoint, int size) throws + InvalidShapeException { + return createGeometryCollection(r, nearPoint, null, size); + } + + public static GeometryCollectionBuilder createGeometryCollectionWithin(Random r, Rectangle within) throws InvalidShapeException { + return createGeometryCollection(r, null, within, 0); + } + + public static GeometryCollectionBuilder createGeometryCollectionWithin(Random r, Rectangle within, int size) throws + InvalidShapeException { + return createGeometryCollection(r, null, within, size); + } + + protected static GeometryCollectionBuilder createGeometryCollection(Random r, Point nearPoint, Rectangle bounds, int numGeometries) + throws InvalidShapeException { + if (numGeometries <= 0) { + // cap geometry collection at 4 shapes (to save test time) + numGeometries = RandomInts.randomIntBetween(r, 2, 5); + } + + if (nearPoint == null) { + nearPoint = xRandomPoint(r); + } + + if (bounds == null) { + bounds = xRandomRectangle(r, nearPoint); + } + + GeometryCollectionBuilder gcb = new GeometryCollectionBuilder(); + for (int i=0; i= 90; + } + + private static Range xRandomRange(Random r, double near, Range bounds) { + double mid = near + r.nextGaussian() * bounds.getWidth() / 6; + double width = Math.abs(r.nextGaussian()) * bounds.getWidth() / 6;//1/3rd + return new Range(mid - width / 2, mid + width / 2); + } + + private static double xDivisible(double v, double divisible) { + return (int) (Math.round(v / divisible) * divisible); + } + + private static double xDivisible(double v) { + return xDivisible(v, xDIVISIBLE); + } + + protected static Rectangle xMakeNormRect(double minX, double maxX, double minY, double maxY) { + minX = DistanceUtils.normLonDEG(minX); + maxX = DistanceUtils.normLonDEG(maxX); + + if (maxX < minX) { + double t = minX; + minX = maxX; + maxX = t; + } + + double minWorldY = ctx.getWorldBounds().getMinY(); + double maxWorldY = ctx.getWorldBounds().getMaxY(); + if (minY < minWorldY || minY > maxWorldY) { + minY = DistanceUtils.normLatDEG(minY); + } + if (maxY < minWorldY || maxY > maxWorldY) { + maxY = DistanceUtils.normLatDEG(maxY); + } + if (maxY < minY) { + double t = minY; + minY = maxY; + maxY = t; + } + return ctx.makeRectangle(minX, maxX, minY, maxY); + } + + protected static double xNormX(double x) { + return ctx.isGeo() ? DistanceUtils.normLonDEG(x) : x; + } + + protected static double xNormY(double y) { + return ctx.isGeo() ? DistanceUtils.normLatDEG(y) : y; + } +}