parent
30f9f278c3
commit
ef5b7412e6
|
@ -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<EmbededPolygonBuilder<MultiPolygonBuilder>> polygons = new ArrayList<EmbededPolygonBuilder<MultiPolygonBuilder>>();
|
||||
|
||||
/**
|
||||
* Add a new polygon to the multipolygon
|
||||
*
|
||||
* @return builder for the new polygon
|
||||
*/
|
||||
public EmbededPolygonBuilder<MultiPolygonBuilder> polygon() {
|
||||
EmbededPolygonBuilder<MultiPolygonBuilder> builder = new EmbededPolygonBuilder<MultiPolygonBuilder>(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<polygons.length; i++) {
|
||||
polygons[i] = this.polygons.get(i).toPolygon();
|
||||
}
|
||||
return GEOMETRY_FACTORY.createMultiPolygon(polygons);
|
||||
}
|
||||
|
||||
public XContentBuilder toXContent(String name, XContentBuilder xcontent) throws IOException {
|
||||
if(name != null) {
|
||||
xcontent.startObject(name);
|
||||
} else {
|
||||
xcontent.startObject();
|
||||
}
|
||||
xcontent.field("type", "multipolygon");
|
||||
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(EmbededPolygonBuilder<MultiPolygonBuilder> 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<PolygonBuilder> {
|
||||
|
||||
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<E> {
|
||||
|
||||
private final List<Point> points = new ArrayList<Point>();
|
||||
private final E parent;
|
||||
private final LinearRingBuilder<EmbededPolygonBuilder<E>> ring = new LinearRingBuilder<EmbededPolygonBuilder<E>>(this);
|
||||
private final ArrayList<LinearRingBuilder<EmbededPolygonBuilder<E>>> holes = new ArrayList<LinearRingBuilder<EmbededPolygonBuilder<E>>>();
|
||||
|
||||
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<E> 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<EmbededPolygonBuilder<E>> hole() {
|
||||
LinearRingBuilder<EmbededPolygonBuilder<E>> builder = new LinearRingBuilder<EmbededPolygonBuilder<E>>(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<E> {
|
||||
|
||||
private final E parent;
|
||||
private final List<Point> points = new ArrayList<Point>();
|
||||
|
||||
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<E> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<String> {
|
|||
@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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
|
|
Binary file not shown.
|
@ -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);
|
||||
|
|
Loading…
Reference in New Issue