From ef5b7412e62c47c933541896564939d37788031b Mon Sep 17 00:00:00 2001 From: Florian Schilling Date: Wed, 10 Apr 2013 12:50:25 +0200 Subject: [PATCH] Allow PolygonBuilder to create polygons with hole Closes #2899 --- .../common/geo/ShapeBuilder.java | 259 +++++++++- .../index/mapper/geo/GeoShapeFieldMapper.java | 5 +- .../index/query/FilterBuilders.java | 54 +- .../index/query/GeoShapeFilterBuilder.java | 39 +- .../search/geo/GeoFilterTests.java | 486 ++++++++++++++++++ .../search/geo/GeoShapeIntegrationTests.java | 8 +- .../integration/search/geo/gzippedmap.json | Bin 0 -> 37943 bytes .../unit/common/geo/ShapeBuilderTests.java | 2 +- 8 files changed, 825 insertions(+), 28 deletions(-) create mode 100644 src/test/java/org/elasticsearch/test/integration/search/geo/GeoFilterTests.java create mode 100644 src/test/java/org/elasticsearch/test/integration/search/geo/gzippedmap.json diff --git a/src/main/java/org/elasticsearch/common/geo/ShapeBuilder.java b/src/main/java/org/elasticsearch/common/geo/ShapeBuilder.java index 33e50460166..ade3fbd613f 100644 --- a/src/main/java/org/elasticsearch/common/geo/ShapeBuilder.java +++ b/src/main/java/org/elasticsearch/common/geo/ShapeBuilder.java @@ -19,6 +19,13 @@ package org.elasticsearch.common.geo; +import org.elasticsearch.ElasticSearchIllegalArgumentException; +import org.elasticsearch.common.xcontent.XContentBuilder; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + import com.spatial4j.core.shape.Point; import com.spatial4j.core.shape.Rectangle; import com.spatial4j.core.shape.Shape; @@ -26,10 +33,12 @@ import com.spatial4j.core.shape.impl.PointImpl; import com.spatial4j.core.shape.impl.RectangleImpl; import com.spatial4j.core.shape.jts.JtsGeometry; import com.spatial4j.core.shape.jts.JtsPoint; -import com.vividsolutions.jts.geom.*; - -import java.util.ArrayList; -import java.util.List; +import com.vividsolutions.jts.geom.Coordinate; +import com.vividsolutions.jts.geom.Geometry; +import com.vividsolutions.jts.geom.GeometryFactory; +import com.vividsolutions.jts.geom.LinearRing; +import com.vividsolutions.jts.geom.MultiPolygon; +import com.vividsolutions.jts.geom.Polygon; /** * Utility class for building {@link Shape} instances like {@link Point}, @@ -71,6 +80,15 @@ public class ShapeBuilder { return new PolygonBuilder(); } + /** + * Creates a new {@link MultiPolygonBuilder} to build a MultiPolygon + * + * @return MultiPolygonBuilder instance + */ + public static MultiPolygonBuilder newMultiPolygon() { + return new MultiPolygonBuilder(); + } + /** * Converts the given Shape into the JTS {@link Geometry} representation. * If the Shape already uses a Geometry, that is returned. @@ -145,12 +163,90 @@ public class ShapeBuilder { } } + /** + * Builder for creating a {@link Shape} instance of a MultiPolygon + */ + public static class MultiPolygonBuilder { + private final ArrayList> polygons = new ArrayList>(); + + /** + * Add a new polygon to the multipolygon + * + * @return builder for the new polygon + */ + public EmbededPolygonBuilder polygon() { + EmbededPolygonBuilder builder = new EmbededPolygonBuilder(this); + polygons.add(builder); + return builder; + } + + public Shape build() { + return new JtsGeometry(toMultiPolygon(), GeoShapeConstants.SPATIAL_CONTEXT, true); + } + + public MultiPolygon toMultiPolygon() { + Polygon[] polygons = new Polygon[this.polygons.size()]; + for (int i = 0; i polygon : polygons) { + polygon.emdedXContent(null, xcontent); + } + xcontent.endArray(); + } + + } + + /** + * Builder for creating a {@link Shape} instance of a single Polygon + */ + public static class PolygonBuilder extends EmbededPolygonBuilder { + + private PolygonBuilder() { + super(null); + } + + @Override + public PolygonBuilder close() { + super.close(); + return this; + } + } + /** * Builder for creating a {@link Shape} instance of a Polygon */ - public static class PolygonBuilder { + public static class EmbededPolygonBuilder { - private final List points = new ArrayList(); + private final E parent; + private final LinearRingBuilder> ring = new LinearRingBuilder>(this); + private final ArrayList>> holes = new ArrayList>>(); + + private EmbededPolygonBuilder(E parent) { + super(); + this.parent = parent; + } /** * Adds a point to the Polygon @@ -159,15 +255,25 @@ public class ShapeBuilder { * @param lat Latitude of the point * @return this */ - public PolygonBuilder point(double lon, double lat) { - points.add(new PointImpl(lon, lat, GeoShapeConstants.SPATIAL_CONTEXT)); + public EmbededPolygonBuilder point(double lon, double lat) { + ring.point(lon, lat); return this; } /** - * Builds a {@link Shape} instance representing the polygon + * Start creating a new hole within the polygon + * @return a builder for holes + */ + public LinearRingBuilder> hole() { + LinearRingBuilder> builder = new LinearRingBuilder>(this); + this.holes.add(builder); + return builder; + } + + /** + * Builds a {@link Shape} instance representing the {@link Polygon} * - * @return Built polygon + * @return Built LinearRing */ public Shape build() { return new JtsGeometry(toPolygon(), GeoShapeConstants.SPATIAL_CONTEXT, true); @@ -179,13 +285,140 @@ public class ShapeBuilder { * @return Built polygon */ public Polygon toPolygon() { + this.ring.close(); + LinearRing ring = this.ring.toLinearRing(); + LinearRing[] rings = new LinearRing[holes.size()]; + for (int i = 0; i < rings.length; i++) { + rings[i] = this.holes.get(i).toLinearRing(); + } + return GEOMETRY_FACTORY.createPolygon(ring, rings); + } + + /** + * Close the linestring by copying the first point if necessary + * @return parent object + */ + public E close() { + this.ring.close(); + return parent; + } + + public XContentBuilder toXContent(String name, XContentBuilder xcontent) throws IOException { + if(name != null) { + xcontent.startObject(name); + } else { + xcontent.startObject(); + } + xcontent.field("type", "polygon"); + emdedXContent("coordinates", xcontent); + xcontent.endObject(); + return xcontent; + } + + protected void emdedXContent(String name, XContentBuilder xcontent) throws IOException { + if(name != null) { + xcontent.startArray(name); + } else { + xcontent.startArray(); + } + ring.emdedXContent(null, xcontent); + for (LinearRingBuilder ring : holes) { + ring.emdedXContent(null, xcontent); + } + xcontent.endArray(); + } + + } + + /** + * Builder for creating a {@link Shape} instance of a Polygon + */ + public static class LinearRingBuilder { + + private final E parent; + private final List points = new ArrayList(); + + private LinearRingBuilder(E parent) { + super(); + this.parent = parent; + } + + /** + * Adds a point to the Ring + * + * @param lon Longitude of the point + * @param lat Latitude of the point + * @return this + */ + public LinearRingBuilder point(double lon, double lat) { + points.add(new PointImpl(lon, lat, GeoShapeConstants.SPATIAL_CONTEXT)); + return this; + } + + /** + * Builds a {@link Shape} instance representing the ring + * + * @return Built LinearRing + */ + protected Shape build() { + return new JtsGeometry(toLinearRing(), GeoShapeConstants.SPATIAL_CONTEXT, true); + } + + /** + * Creates the raw {@link Polygon} + * + * @return Built LinearRing + */ + protected LinearRing toLinearRing() { + this.close(); Coordinate[] coordinates = new Coordinate[points.size()]; - for (int i = 0; i < points.size(); i++) { + for (int i = 0; i < coordinates.length; i++) { coordinates[i] = new Coordinate(points.get(i).getX(), points.get(i).getY()); } - LinearRing ring = GEOMETRY_FACTORY.createLinearRing(coordinates); - return GEOMETRY_FACTORY.createPolygon(ring, null); + return GEOMETRY_FACTORY.createLinearRing(coordinates); + } + + /** + * Close the linestring by copying the first point if necessary + * @return parent object + */ + public E close() { + Point first = points.get(0); + Point last = points.get(points.size()-1); + if(first.getX() != last.getX() || first.getY() != last.getY()) { + points.add(first); + } + + if(points.size()<4) { + throw new ElasticSearchIllegalArgumentException("A linear ring is defined by a least four points"); + } + + return parent; + } + + public XContentBuilder toXContent(String name, XContentBuilder xcontent) throws IOException { + if(name != null) { + xcontent.startObject(name); + } else { + xcontent.startObject(); + } + xcontent.field("type", "linestring"); + emdedXContent("coordinates", xcontent); + xcontent.endObject(); + return xcontent; + } + + protected void emdedXContent(String name, XContentBuilder xcontent) throws IOException { + if(name != null) { + xcontent.startArray(name); + } else { + xcontent.startArray(); + } + for(Point point : points) { + xcontent.startArray().value(point.getY()).value(point.getX()).endArray(); + } + xcontent.endArray(); } } } diff --git a/src/main/java/org/elasticsearch/index/mapper/geo/GeoShapeFieldMapper.java b/src/main/java/org/elasticsearch/index/mapper/geo/GeoShapeFieldMapper.java index 78461e7e375..42778106b5e 100644 --- a/src/main/java/org/elasticsearch/index/mapper/geo/GeoShapeFieldMapper.java +++ b/src/main/java/org/elasticsearch/index/mapper/geo/GeoShapeFieldMapper.java @@ -43,6 +43,8 @@ import org.elasticsearch.index.mapper.MapperParsingException; import org.elasticsearch.index.mapper.ParseContext; import org.elasticsearch.index.mapper.core.AbstractFieldMapper; +import com.spatial4j.core.shape.Shape; + import java.io.IOException; import java.util.Map; @@ -213,7 +215,8 @@ public class GeoShapeFieldMapper extends AbstractFieldMapper { @Override public void parse(ParseContext context) throws IOException { try { - Field[] fields = defaultStrategy.createIndexableFields(GeoJSONShapeParser.parse(context.parser())); + Shape shape = GeoJSONShapeParser.parse(context.parser()); + Field[] fields = defaultStrategy.createIndexableFields(shape); if (fields == null || fields.length == 0) { return; } diff --git a/src/main/java/org/elasticsearch/index/query/FilterBuilders.java b/src/main/java/org/elasticsearch/index/query/FilterBuilders.java index d6c96ed851c..71f8bcda771 100644 --- a/src/main/java/org/elasticsearch/index/query/FilterBuilders.java +++ b/src/main/java/org/elasticsearch/index/query/FilterBuilders.java @@ -21,6 +21,7 @@ package org.elasticsearch.index.query; import com.spatial4j.core.shape.Shape; import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.geo.ShapeRelation; /** * A static factory for simple "import static" usage. @@ -358,17 +359,60 @@ public abstract class FilterBuilders { } /** - * A filter to filter based on the relationship between a shape and indexed shapes + * A filter based on the relationship of a shape and indexed shapes + * + * @param name The shape field name + * @param shape Shape to use in the filter + * @param relation relation of the shapes + */ + public static GeoShapeFilterBuilder geoShapeFilter(String name, Shape shape, ShapeRelation relation) { + return new GeoShapeFilterBuilder(name, shape, relation); + } + + public static GeoShapeFilterBuilder geoShapeFilter(String name, String indexedShapeId, String indexedShapeType, ShapeRelation relation) { + return new GeoShapeFilterBuilder(name, indexedShapeId, indexedShapeType, relation); + } + + /** + * A filter to filter indexed shapes intersecting with shapes * * @param name The shape field name * @param shape Shape to use in the filter */ - public static GeoShapeFilterBuilder geoShapeFilter(String name, Shape shape) { - return new GeoShapeFilterBuilder(name, shape); + public static GeoShapeFilterBuilder geoIntersectionFilter(String name, Shape shape) { + return geoShapeFilter(name, shape, ShapeRelation.INTERSECTS); } - public static GeoShapeFilterBuilder geoShapeFilter(String name, String indexedShapeId, String indexedShapeType) { - return new GeoShapeFilterBuilder(name, indexedShapeId, indexedShapeType); + public static GeoShapeFilterBuilder geoIntersectionFilter(String name, String indexedShapeId, String indexedShapeType) { + return geoShapeFilter(name, indexedShapeId, indexedShapeType, ShapeRelation.INTERSECTS); + } + + /** + * A filter to filter indexed shapes that are contained by a shape + * + * @param name The shape field name + * @param shape Shape to use in the filter + */ + public static GeoShapeFilterBuilder geoWithinFilter(String name, Shape shape) { + return geoShapeFilter(name, shape, ShapeRelation.WITHIN); + } + + public static GeoShapeFilterBuilder geoWithinFilter(String name, String indexedShapeId, String indexedShapeType) { + return geoShapeFilter(name, indexedShapeId, indexedShapeType, ShapeRelation.WITHIN); + } + + /** + * A filter to filter indexed shapes that are not intersection with the query shape + * + * @param name The shape field name + * @param shape Shape to use in the filter + */ + public static GeoShapeFilterBuilder geoDisjointFilter(String name, Shape shape) { + return geoShapeFilter(name, shape, ShapeRelation.DISJOINT); + } + + public static GeoShapeFilterBuilder geoDisjointFilter(String name, String indexedShapeId, String indexedShapeType) { + return geoShapeFilter(name, indexedShapeId, indexedShapeType, ShapeRelation.DISJOINT); } /** diff --git a/src/main/java/org/elasticsearch/index/query/GeoShapeFilterBuilder.java b/src/main/java/org/elasticsearch/index/query/GeoShapeFilterBuilder.java index ce0b7c20794..e0807624446 100644 --- a/src/main/java/org/elasticsearch/index/query/GeoShapeFilterBuilder.java +++ b/src/main/java/org/elasticsearch/index/query/GeoShapeFilterBuilder.java @@ -21,6 +21,7 @@ package org.elasticsearch.index.query; import com.spatial4j.core.shape.Shape; import org.elasticsearch.common.geo.GeoJSONShapeSerializer; +import org.elasticsearch.common.geo.ShapeRelation; import org.elasticsearch.common.geo.SpatialStrategy; import org.elasticsearch.common.xcontent.XContentBuilder; @@ -48,6 +49,8 @@ public class GeoShapeFilterBuilder extends BaseFilterBuilder { private String indexedShapeIndex; private String indexedShapeFieldName; + private ShapeRelation relation = null; + /** * Creates a new GeoShapeFilterBuilder whose Filter will be against the * given field name using the given Shape @@ -56,7 +59,19 @@ public class GeoShapeFilterBuilder extends BaseFilterBuilder { * @param shape Shape used in the filter */ public GeoShapeFilterBuilder(String name, Shape shape) { - this(name, shape, null, null); + this(name, shape, null, null, null); + } + + /** + * Creates a new GeoShapeFilterBuilder whose Filter will be against the + * given field name using the given Shape + * + * @param name Name of the field that will be filtered + * @param relation {@link ShapeRelation} of query and indexed shape + * @param shape Shape used in the filter + */ + public GeoShapeFilterBuilder(String name, Shape shape, ShapeRelation relation) { + this(name, shape, null, null, relation); } /** @@ -67,14 +82,15 @@ public class GeoShapeFilterBuilder extends BaseFilterBuilder { * @param indexedShapeId ID of the indexed Shape that will be used in the Filter * @param indexedShapeType Index type of the indexed Shapes */ - public GeoShapeFilterBuilder(String name, String indexedShapeId, String indexedShapeType) { - this(name, null, indexedShapeId, indexedShapeType); + public GeoShapeFilterBuilder(String name, String indexedShapeId, String indexedShapeType, ShapeRelation relation) { + this(name, null, indexedShapeId, indexedShapeType, relation); } - private GeoShapeFilterBuilder(String name, Shape shape, String indexedShapeId, String indexedShapeType) { + private GeoShapeFilterBuilder(String name, Shape shape, String indexedShapeId, String indexedShapeType, ShapeRelation relation) { this.name = name; this.shape = shape; this.indexedShapeId = indexedShapeId; + this.relation = relation; this.indexedShapeType = indexedShapeType; } @@ -145,6 +161,17 @@ public class GeoShapeFilterBuilder extends BaseFilterBuilder { return this; } + /** + * Sets the relation of query shape and indexed shape. + * + * @param relation relation of the shapes + * @return this + */ + public GeoShapeFilterBuilder relation(ShapeRelation relation) { + this.relation = relation; + return this; + } + @Override protected void doXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(GeoShapeFilterParser.NAME); @@ -172,6 +199,10 @@ public class GeoShapeFilterBuilder extends BaseFilterBuilder { builder.endObject(); } + if(relation != null) { + builder.field("relation", relation.getRelationName()); + } + builder.endObject(); if (name != null) { diff --git a/src/test/java/org/elasticsearch/test/integration/search/geo/GeoFilterTests.java b/src/test/java/org/elasticsearch/test/integration/search/geo/GeoFilterTests.java new file mode 100644 index 00000000000..c77a97ac893 --- /dev/null +++ b/src/test/java/org/elasticsearch/test/integration/search/geo/GeoFilterTests.java @@ -0,0 +1,486 @@ +/* + * Licensed to ElasticSearch and Shay Banon 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.integration.search.geo; + +import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; +import static org.elasticsearch.index.query.FilterBuilders.geoBoundingBoxFilter; +import static org.elasticsearch.index.query.FilterBuilders.geoDistanceFilter; +import static org.elasticsearch.index.query.QueryBuilders.fieldQuery; +import static org.elasticsearch.index.query.QueryBuilders.filteredQuery; +import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.anyOf; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.lessThanOrEqualTo; + +import java.io.ByteArrayOutputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.util.zip.GZIPInputStream; + +import org.elasticsearch.action.admin.indices.create.CreateIndexRequestBuilder; +import org.elasticsearch.action.bulk.BulkItemResponse; +import org.elasticsearch.action.bulk.BulkResponse; +import org.elasticsearch.action.search.SearchResponse; +import org.elasticsearch.client.Client; +import org.elasticsearch.common.Priority; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.geo.GeoPoint; +import org.elasticsearch.common.geo.GeoUtils; +import org.elasticsearch.common.geo.ShapeBuilder; +import org.elasticsearch.common.geo.ShapeBuilder.MultiPolygonBuilder; +import org.elasticsearch.common.geo.ShapeBuilder.PolygonBuilder; +import org.elasticsearch.common.io.Streams; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.index.query.FilterBuilders; +import org.elasticsearch.search.SearchHit; +import org.elasticsearch.test.integration.AbstractNodesTests; + +import org.apache.lucene.spatial.prefix.RecursivePrefixTreeStrategy; +import org.apache.lucene.spatial.prefix.tree.GeohashPrefixTree; +import org.apache.lucene.spatial.prefix.tree.Node; +import org.apache.lucene.spatial.prefix.tree.QuadPrefixTree; +import org.apache.lucene.spatial.query.SpatialArgs; +import org.apache.lucene.spatial.query.SpatialOperation; +import org.apache.lucene.spatial.query.UnsupportedSpatialOperation; + +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import com.spatial4j.core.context.SpatialContext; +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.Shape; + +/** + * + */ +public class GeoFilterTests extends AbstractNodesTests { + + private Client client; + + private boolean intersectSupport; + private boolean disjointSupport; + private boolean withinSupport; + + @BeforeClass + public void createNodes() throws Exception { + startNode("server1"); + startNode("server2"); + + intersectSupport = testRelationSupport(SpatialOperation.Intersects); + disjointSupport = testRelationSupport(SpatialOperation.IsDisjointTo); + withinSupport = testRelationSupport(SpatialOperation.IsWithin); + + client = getClient(); + } + + private static byte[] unZipData(String path) throws IOException { + InputStream is = Streams.class.getResourceAsStream(path); + if (is == null) { + throw new FileNotFoundException("Resource [" + path + "] not found in classpath"); + } + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + GZIPInputStream in = new GZIPInputStream(is); + Streams.copy(in, out); + + is.close(); + out.close(); + + return out.toByteArray(); + } + + @AfterClass + public void closeNodes() { + client.close(); + closeAllNodes(); + } + + protected Client getClient() { + return client("server1"); + } + + @Test + public void testShapeBuilders() { + + try { + // self intersection polygon + ShapeBuilder.newPolygon() + .point(-10, -10) + .point(10, 10) + .point(-10, 10) + .point(10, -10) + .close().build(); + assert false : "Self intersection not detected"; + } catch (InvalidShapeException e) {} + + // polygon with hole + ShapeBuilder.newPolygon() + .point(-10, -10).point(-10, 10).point(10, 10).point(10, -10) + .hole() + .point(-5, -5).point(-5, 5).point(5, 5).point(5, -5) + .close().close().build(); + + try { + // polygon with overlapping hole + ShapeBuilder.newPolygon() + .point(-10, -10).point(-10, 10).point(10, 10).point(10, -10) + .hole() + .point(-5, -5).point(-5, 11).point(5, 11).point(5, -5) + .close().close().build(); + + assert false : "Self intersection not detected"; + } catch (InvalidShapeException e) {} + + try { + // polygon with intersection holes + ShapeBuilder.newPolygon() + .point(-10, -10).point(-10, 10).point(10, 10).point(10, -10) + .hole() + .point(-5, -5).point(-5, 5).point(5, 5).point(5, -5) + .close() + .hole() + .point(-5, -6).point(5, -6).point(5, -4).point(-5, -4) + .close() + .close().build(); + assert false : "Intersection of holes not detected"; + } catch (InvalidShapeException e) {} + + try { + // Common line in polygon + ShapeBuilder.newPolygon() + .point(-10, -10) + .point(-10, 10) + .point(-5, 10) + .point(-5, -5) + .point(-5, 20) + .point(10, 20) + .point(10, -10) + .close().build(); + assert false : "Self intersection not detected"; + } catch (InvalidShapeException e) {} + +// Not specified +// try { +// // two overlapping polygons within a multipolygon +// ShapeBuilder.newMultiPolygon() +// .polygon() +// .point(-10, -10) +// .point(-10, 10) +// .point(10, 10) +// .point(10, -10) +// .close() +// .polygon() +// .point(-5, -5).point(-5, 5).point(5, 5).point(5, -5) +// .close().build(); +// assert false : "Polygon intersection not detected"; +// } catch (InvalidShapeException e) {} + + // Multipolygon: polygon with hole and polygon within the whole + ShapeBuilder.newMultiPolygon() + .polygon() + .point(-10, -10).point(-10, 10).point(10, 10).point(10, -10) + .hole() + .point(-5, -5).point(-5, 5).point(5, 5).point(5, -5) + .close() + .close() + .polygon() + .point(-4, -4).point(-4, 4).point(4, 4).point(4, -4) + .close() + .build(); + +// Not supported +// try { +// // Multipolygon: polygon with hole and polygon within the hole but overlapping +// ShapeBuilder.newMultiPolygon() +// .polygon() +// .point(-10, -10).point(-10, 10).point(10, 10).point(10, -10) +// .hole() +// .point(-5, -5).point(-5, 5).point(5, 5).point(5, -5) +// .close() +// .close() +// .polygon() +// .point(-4, -4).point(-4, 6).point(4, 6).point(4, -4) +// .close() +// .build(); +// assert false : "Polygon intersection not detected"; +// } catch (InvalidShapeException e) {} + + } + + @Test + public void testShapeRelations() throws Exception { + + assert intersectSupport: "Intersect relation is not supported"; +// assert disjointSupport: "Disjoint relation is not supported"; +// assert withinSupport: "within relation is not supported"; + + assert !disjointSupport: "Disjoint relation is now supported"; + assert !withinSupport: "within relation is now supported"; + + String mapping = XContentFactory.jsonBuilder() + .startObject() + .startObject("polygon") + .startObject("properties") + .startObject("area") + .field("type", "geo_shape") + .field("tree", "geohash") + .field("store", true) + .endObject() + .endObject() + .endObject() + .endObject().string(); + + CreateIndexRequestBuilder mappingRequest = client.admin().indices().prepareCreate("shapes").addMapping("polygon", mapping); + mappingRequest.execute().actionGet(); + client.admin().cluster().prepareHealth().setWaitForEvents(Priority.LANGUID).setWaitForGreenStatus().execute().actionGet(); + + // Create a multipolygon with two polygons. The first is an rectangle of size 10x10 + // with a hole of size 5x5 equidistant from all sides. This hole in turn contains + // the second polygon of size 4x4 equidistant from all sites + MultiPolygonBuilder polygon = ShapeBuilder.newMultiPolygon() + .polygon() + .point(-10, -10).point(-10, 10).point(10, 10).point(10, -10) + .hole() + .point(-5, -5).point(-5, 5).point(5, 5).point(5, -5) + .close() + .close() + .polygon() + .point(-4, -4).point(-4, 4).point(4, 4).point(4, -4) + .close(); + + BytesReference data = polygon.toXContent("area", jsonBuilder().startObject()).endObject().bytes(); + client.prepareIndex("shapes", "polygon", "1").setSource(data).execute().actionGet(); + client.admin().indices().prepareRefresh().execute().actionGet(); + + // Point in polygon + SearchResponse result = client.prepareSearch() + .setQuery(matchAllQuery()) + .setFilter(FilterBuilders.geoIntersectionFilter("area", ShapeBuilder.newPoint(3, 3))) + .execute().actionGet(); + assertThat(result.getHits().getTotalHits(), equalTo(1L)); + assertThat(result.getHits().getAt(0).getId(), equalTo("1")); + + // Point in polygon hole + result = client.prepareSearch() + .setQuery(matchAllQuery()) + .setFilter(FilterBuilders.geoIntersectionFilter("area", ShapeBuilder.newPoint(4.5, 4.5))) + .execute().actionGet(); + assertThat(result.getHits().getTotalHits(), equalTo(0L)); + + // by definition the border of a polygon belongs to the inner + // so the border of a polygons hole also belongs to the inner + // of the polygon NOT the hole + + // Point on polygon border + result = client.prepareSearch() + .setQuery(matchAllQuery()) + .setFilter(FilterBuilders.geoIntersectionFilter("area", ShapeBuilder.newPoint(10.0, 5.0))) + .execute().actionGet(); + assertThat(result.getHits().getTotalHits(), equalTo(1L)); + assertThat(result.getHits().getAt(0).getId(), equalTo("1")); + + // Point on hole border + result = client.prepareSearch() + .setQuery(matchAllQuery()) + .setFilter(FilterBuilders.geoIntersectionFilter("area", ShapeBuilder.newPoint(5.0, 2.0))) + .execute().actionGet(); + assertThat(result.getHits().getTotalHits(), equalTo(1L)); + assertThat(result.getHits().getAt(0).getId(), equalTo("1")); + + if(disjointSupport) { + // Point not in polygon + result = client.prepareSearch() + .setQuery(matchAllQuery()) + .setFilter(FilterBuilders.geoDisjointFilter("area", ShapeBuilder.newPoint(3, 3))) + .execute().actionGet(); + assertThat(result.getHits().getTotalHits(), equalTo(0L)); + + // Point in polygon hole + result = client.prepareSearch() + .setQuery(matchAllQuery()) + .setFilter(FilterBuilders.geoDisjointFilter("area", ShapeBuilder.newPoint(4.5, 4.5))) + .execute().actionGet(); + assertThat(result.getHits().getTotalHits(), equalTo(1L)); + assertThat(result.getHits().getAt(0).getId(), equalTo("1")); + } + + // Create a polygon that fills the empty area of the polygon defined above + PolygonBuilder inverse = ShapeBuilder.newPolygon() + .point(-5, -5).point(-5, 5).point(5, 5).point(5, -5) + .hole() + .point(-4, -4).point(-4, 4).point(4, 4).point(4, -4) + .close() + .close(); + + data = inverse.toXContent("area", jsonBuilder().startObject()).endObject().bytes(); + client.prepareIndex("shapes", "polygon", "2").setSource(data).execute().actionGet(); + client.admin().indices().prepareRefresh().execute().actionGet(); + + // re-check point on polygon hole + result = client.prepareSearch() + .setQuery(matchAllQuery()) + .setFilter(FilterBuilders.geoIntersectionFilter("area", ShapeBuilder.newPoint(4.5, 4.5))) + .execute().actionGet(); + assertThat(result.getHits().getTotalHits(), equalTo(1L)); + assertThat(result.getHits().getAt(0).getId(), equalTo("2")); + + // Create Polygon with hole and common edge + PolygonBuilder builder = ShapeBuilder.newPolygon() + .point(-10, -10).point(-10, 10).point(10, 10).point(10, -10) + .hole() + .point(-5, -5).point(-5, 5).point(10, 5).point(10, -5) + .close() + .close(); + + if(withinSupport) { + // Polygon WithIn Polygon + builder = ShapeBuilder.newPolygon() + .point(-30, -30).point(-30, 30).point(30, 30).point(30, -30).close(); + + result = client.prepareSearch() + .setQuery(matchAllQuery()) + .setFilter(FilterBuilders.geoWithinFilter("area", builder.build())) + .execute().actionGet(); + assertThat(result.getHits().getTotalHits(), equalTo(2L)); + } + +/* TODO: fix Polygon builder! It is not possible to cross the lats -180 and 180. + * A simple solution is following the path that is currently set up. When + * it's crossing the 180° lat set the new point to the intersection of line- + * segment and longitude and start building a new Polygon on the other side + * of the latitude. When crossing the latitude again continue drawing the + * first polygon. This approach can also applied to the holes because the + * commonline of hole and polygon will not be recognized as intersection. + */ + +// // Create a polygon crossing longitude 180. +// builder = ShapeBuilder.newPolygon() +// .point(170, -10).point(180, 10).point(170, -10).point(10, -10) +// .close(); +// +// data = builder.toXContent("area", jsonBuilder().startObject()).endObject().bytes(); +// client.prepareIndex("shapes", "polygon", "1").setSource(data).execute().actionGet(); +// client.admin().indices().prepareRefresh().execute().actionGet(); + } + + @Test + public void bulktest() throws Exception { + byte[] bulkAction = unZipData("/org/elasticsearch/test/integration/search/geo/gzippedmap.json"); + + String mapping = XContentFactory.jsonBuilder() + .startObject() + .startObject("country") + .startObject("properties") + .startObject("pin") + .field("type", "geo_point") + .field("lat_lon", true) + .field("store", true) + .endObject() + .startObject("location") + .field("type", "geo_shape") + .field("lat_lon", true) + .field("store", true) + .endObject() + .endObject() + .endObject() + .endObject() + .string(); + + client.admin().indices().prepareCreate("countries").addMapping("country", mapping).execute().actionGet(); + BulkResponse bulk = client.prepareBulk().add(bulkAction, 0, bulkAction.length, false, null, null).execute().actionGet(); + + for(BulkItemResponse item : bulk.getItems()) { + assert !item.isFailed(): "unable to index data"; + } + + client.admin().indices().prepareRefresh().execute().actionGet(); + String key = "DE"; + + SearchResponse searchResponse = client.prepareSearch() + .setQuery(fieldQuery("_id", key)) + .execute().actionGet(); + + assertThat(searchResponse.getHits().getTotalHits(), equalTo(1L)); + + for (SearchHit hit : searchResponse.getHits()) { + assertThat(hit.getId(), equalTo(key)); + } + + SearchResponse world = client.prepareSearch().addField("pin").setQuery( + filteredQuery( + matchAllQuery(), + geoBoundingBoxFilter("pin") + .topLeft(90, -179.99999) + .bottomRight(-90, 179.99999)) + ).execute().actionGet(); + + assertThat(world.getHits().totalHits(), equalTo(246L)); + + SearchResponse distance = client.prepareSearch().addField("pin").setQuery( + filteredQuery( + matchAllQuery(), + geoDistanceFilter("pin").distance("425km").point(51.11, 9.851) + )).execute().actionGet(); + + assertThat(distance.getHits().totalHits(), equalTo(5L)); + GeoPoint point = new GeoPoint(); + for (SearchHit hit : distance.getHits()) { + String name = hit.getId(); + point.resetFromString(hit.fields().get("pin").getValue().toString()); + double dist = distance(point.getLat(), point.getLon(), 51.11, 9.851); + + assertThat("distance to '" + name + "'", dist, lessThanOrEqualTo(425000d)); + assertThat(name, anyOf(equalTo("CZ"), equalTo("DE"), equalTo("BE"), equalTo("NL"), equalTo("LU"))); + if(key.equals(name)) { + assertThat(dist, equalTo(0d)); + } + } + } + + public static double distance(double lat1, double lon1, double lat2, double lon2) { + return GeoUtils.EARTH_SEMI_MAJOR_AXIS * DistanceUtils.distHaversineRAD( + DistanceUtils.toRadians(lat1), + DistanceUtils.toRadians(lon1), + DistanceUtils.toRadians(lat2), + DistanceUtils.toRadians(lon2) + ); + } + + protected static boolean testRelationSupport(SpatialOperation relation) { + try { + GeohashPrefixTree tree = new GeohashPrefixTree(SpatialContext.GEO, 3); + RecursivePrefixTreeStrategy strategy = new RecursivePrefixTreeStrategy(tree, "area"); + Shape shape = SpatialContext.GEO.makePoint(0, 0); + SpatialArgs args = new SpatialArgs(relation, shape); + strategy.makeFilter(args); + return true; + } catch (UnsupportedSpatialOperation e) { + return false; + } + } +} + diff --git a/src/test/java/org/elasticsearch/test/integration/search/geo/GeoShapeIntegrationTests.java b/src/test/java/org/elasticsearch/test/integration/search/geo/GeoShapeIntegrationTests.java index a15f3fe4bdc..909c1695ba0 100644 --- a/src/test/java/org/elasticsearch/test/integration/search/geo/GeoShapeIntegrationTests.java +++ b/src/test/java/org/elasticsearch/test/integration/search/geo/GeoShapeIntegrationTests.java @@ -33,7 +33,7 @@ import org.testng.annotations.Test; import static org.elasticsearch.common.geo.ShapeBuilder.newRectangle; import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; -import static org.elasticsearch.index.query.FilterBuilders.geoShapeFilter; +import static org.elasticsearch.index.query.FilterBuilders.*; import static org.elasticsearch.index.query.QueryBuilders.*; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; @@ -94,7 +94,7 @@ public class GeoShapeIntegrationTests extends AbstractNodesTests { SearchResponse searchResponse = client.prepareSearch() .setQuery(filteredQuery(matchAllQuery(), - geoShapeFilter("location", shape))) + geoIntersectionFilter("location", shape))) .execute().actionGet(); assertThat(searchResponse.getHits().getTotalHits(), equalTo(1l)); @@ -144,7 +144,7 @@ public class GeoShapeIntegrationTests extends AbstractNodesTests { // used the bottom-level optimization in SpatialPrefixTree#recursiveGetNodes. SearchResponse searchResponse = client.prepareSearch() .setQuery(filteredQuery(matchAllQuery(), - geoShapeFilter("location", query))) + geoIntersectionFilter("location", query))) .execute().actionGet(); assertThat(searchResponse.getHits().getTotalHits(), equalTo(1l)); @@ -188,7 +188,7 @@ public class GeoShapeIntegrationTests extends AbstractNodesTests { SearchResponse searchResponse = client.prepareSearch("test") .setQuery(filteredQuery(matchAllQuery(), - geoShapeFilter("location", "Big_Rectangle", "shape_type"))) + geoIntersectionFilter("location", "Big_Rectangle", "shape_type"))) .execute().actionGet(); assertThat(searchResponse.getHits().getTotalHits(), equalTo(1l)); diff --git a/src/test/java/org/elasticsearch/test/integration/search/geo/gzippedmap.json b/src/test/java/org/elasticsearch/test/integration/search/geo/gzippedmap.json new file mode 100644 index 0000000000000000000000000000000000000000..2e29891105edb836d6212981395c597e6e456903 GIT binary patch literal 37943 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 DH}lGd literal 0 HcmV?d00001 diff --git a/src/test/java/org/elasticsearch/test/unit/common/geo/ShapeBuilderTests.java b/src/test/java/org/elasticsearch/test/unit/common/geo/ShapeBuilderTests.java index f9f18d8258b..e397bfa7288 100644 --- a/src/test/java/org/elasticsearch/test/unit/common/geo/ShapeBuilderTests.java +++ b/src/test/java/org/elasticsearch/test/unit/common/geo/ShapeBuilderTests.java @@ -56,7 +56,7 @@ public class ShapeBuilderTests { .point(45, 30) .point(45, -30) .point(-45, -30) - .point(-45, 30); + .close(); Shape polygon = polygonBuilder.build(); Geometry polygonGeometry = ShapeBuilder.toJTSGeometry(polygon);