Geo: Adds support for GeoJSON GeometryCollection

Closes #2796
This commit is contained in:
Colin Goodheart-Smithe 2014-08-01 14:05:18 +01:00
parent 76b7b68605
commit b2286915cd
6 changed files with 303 additions and 5 deletions

View File

@ -143,13 +143,13 @@ polygon and a minimum of `4` vertices.
points.
|`MultiLineString` |`multilinestring` |An array of separate linestrings.
|`MultiPolygon` |`multipolygon` |An array of separate polygons.
|`GeometryCollection` |`geometrycollection` | A GeoJSON shape similar to the
`multi*` shapes except that multiple types can coexist (e.g., a Point
and a LineString).
|`N/A` |`envelope` |A bounding rectangle, or envelope, specified by
specifying only the top left and bottom right points.
|`N/A` |`circle` |A circle specified by a center point and radius with
units, which default to `METERS`.
|`GeometryCollection` |`N/A` | An unsupported GeoJSON shape similar to the
`multi*` shapes except that multiple types can coexist (e.g., a Point
and a LineString).
|=======================================================================
[NOTE]
@ -291,6 +291,31 @@ A list of geojson polygons.
}
--------------------------------------------------
[float]
===== http://geojson.org/geojson-spec.html#geometrycollection[Geometry Collection]
A collection of geojson geometry objects.
[source,js]
--------------------------------------------------
{
"location" : {
"type": "geometrycollection",
"geometries": [
{
"type": "point",
"coordinates": [100.0, 0.0]
},
{
"type": "linestring",
"coordinates": [ [101.0, 0.0], [102.0, 1.0] ]
}
]
}
}
--------------------------------------------------
[float]
===== Envelope

View File

@ -0,0 +1,114 @@
/*
* 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.common.geo.builders;
import com.spatial4j.core.shape.Shape;
import com.spatial4j.core.shape.ShapeCollection;
import org.elasticsearch.common.xcontent.XContentBuilder;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class GeometryCollectionBuilder extends ShapeBuilder {
public static final GeoShapeType TYPE = GeoShapeType.GEOMETRYCOLLECTION;
protected final ArrayList<ShapeBuilder> shapes = new ArrayList<>();
public GeometryCollectionBuilder shape(ShapeBuilder shape) {
this.shapes.add(shape);
return this;
}
public GeometryCollectionBuilder point(PointBuilder point) {
this.shapes.add(point);
return this;
}
public GeometryCollectionBuilder multiPoint(MultiPointBuilder multiPoint) {
this.shapes.add(multiPoint);
return this;
}
public GeometryCollectionBuilder line(BaseLineStringBuilder<?> line) {
this.shapes.add(line);
return this;
}
public GeometryCollectionBuilder multiLine(MultiLineStringBuilder multiLine) {
this.shapes.add(multiLine);
return this;
}
public GeometryCollectionBuilder polygon(BasePolygonBuilder<?> polygon) {
this.shapes.add(polygon);
return this;
}
public GeometryCollectionBuilder multiPolygon(MultiPolygonBuilder multiPolygon) {
this.shapes.add(multiPolygon);
return this;
}
public GeometryCollectionBuilder envelope(EnvelopeBuilder envelope) {
this.shapes.add(envelope);
return this;
}
public GeometryCollectionBuilder circle(CircleBuilder circle) {
this.shapes.add(circle);
return this;
}
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject();
builder.field(FIELD_TYPE, TYPE.shapename);
builder.startArray(FIELD_GEOMETRIES);
for (ShapeBuilder shape : shapes) {
shape.toXContent(builder, params);
}
builder.endArray();
return builder.endObject();
}
@Override
public GeoShapeType type() {
return TYPE;
}
@Override
public Shape build() {
List<Shape> shapes = new ArrayList<>(this.shapes.size());
for (ShapeBuilder shape : this.shapes) {
shapes.add(shape.build());
}
if (shapes.size() == 1)
return shapes.get(0);
else
return new ShapeCollection<>(shapes, SPATIAL_CONTEXT);
//note: ShapeCollection is probably faster than a Multi* geom.
}
}

View File

@ -150,6 +150,14 @@ public abstract class ShapeBuilder implements ToXContent {
return new MultiPolygonBuilder();
}
/**
* Create a new GeometryCollection
* @return a new {@link GeometryCollectionBuilder}
*/
public static GeometryCollectionBuilder newGeometryCollection() {
return new GeometryCollectionBuilder();
}
/**
* create a new Circle
* @return a new {@link CircleBuilder}
@ -498,6 +506,7 @@ public abstract class ShapeBuilder implements ToXContent {
public static final String FIELD_TYPE = "type";
public static final String FIELD_COORDINATES = "coordinates";
public static final String FIELD_GEOMETRIES = "geometries";
protected static final boolean debugEnabled() {
return LOGGER.isDebugEnabled() || DEBUG;
@ -513,6 +522,7 @@ public abstract class ShapeBuilder implements ToXContent {
MULTILINESTRING("multilinestring"),
POLYGON("polygon"),
MULTIPOLYGON("multipolygon"),
GEOMETRYCOLLECTION("geometrycollection"),
ENVELOPE("envelope"),
CIRCLE("circle");
@ -542,6 +552,7 @@ public abstract class ShapeBuilder implements ToXContent {
GeoShapeType shapeType = null;
Distance radius = null;
CoordinateNode node = null;
GeometryCollectionBuilder geometryCollections = null;
XContentParser.Token token;
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
@ -554,6 +565,9 @@ public abstract class ShapeBuilder implements ToXContent {
} else if (FIELD_COORDINATES.equals(fieldName)) {
parser.nextToken();
node = parseCoordinates(parser);
} else if (FIELD_GEOMETRIES.equals(fieldName)) {
parser.nextToken();
geometryCollections = parseGeometries(parser);
} else if (CircleBuilder.FIELD_RADIUS.equals(fieldName)) {
parser.nextToken();
radius = Distance.parseDistance(parser.text());
@ -566,8 +580,10 @@ public abstract class ShapeBuilder implements ToXContent {
if (shapeType == null) {
throw new ElasticsearchParseException("Shape type not included");
} else if (node == null) {
} else if (node == null && GeoShapeType.GEOMETRYCOLLECTION != shapeType) {
throw new ElasticsearchParseException("Coordinates not included");
} else if (geometryCollections == null && GeoShapeType.GEOMETRYCOLLECTION == shapeType) {
throw new ElasticsearchParseException("geometries not included");
} else if (radius != null && GeoShapeType.CIRCLE != shapeType) {
throw new ElasticsearchParseException("Field [" + CircleBuilder.FIELD_RADIUS + "] is supported for [" + CircleBuilder.TYPE
+ "] only");
@ -582,6 +598,7 @@ public abstract class ShapeBuilder implements ToXContent {
case MULTIPOLYGON: return parseMultiPolygon(node);
case CIRCLE: return parseCircle(node, radius);
case ENVELOPE: return parseEnvelope(node);
case GEOMETRYCOLLECTION: return geometryCollections;
default:
throw new ElasticsearchParseException("Shape type [" + shapeType + "] not included");
}
@ -639,5 +656,28 @@ public abstract class ShapeBuilder implements ToXContent {
}
return polygons;
}
/**
* Parse the geometries array of a GeometryCollection
*
* @param parser Parser that will be read from
* @return Geometry[] geometries of the GeometryCollection
* @throws IOException Thrown if an error occurs while reading from the XContentParser
*/
protected static GeometryCollectionBuilder parseGeometries(XContentParser parser) throws IOException {
if (parser.currentToken() != XContentParser.Token.START_ARRAY) {
throw new ElasticsearchParseException("Geometries must be an array of geojson objects");
}
XContentParser.Token token = parser.nextToken();
GeometryCollectionBuilder geometryCollection = newGeometryCollection();
while (token != XContentParser.Token.END_ARRAY) {
ShapeBuilder shapeBuilder = GeoShapeType.parse(parser);
geometryCollection.shape(shapeBuilder);
token = parser.nextToken();
}
return geometryCollection;
}
}
}

View File

@ -75,6 +75,34 @@ public class GeoJSONShapeParserTests extends ElasticsearchTestCase {
assertGeometryEquals(jtsGeom(expected), lineGeoJson);
}
@Test
public void testParse_multiLineString() throws IOException {
String multilinesGeoJson = XContentFactory.jsonBuilder().startObject().field("type", "MultiLineString")
.startArray("coordinates")
.startArray()
.startArray().value(100.0).value(0.0).endArray()
.startArray().value(101.0).value(1.0).endArray()
.endArray()
.startArray()
.startArray().value(102.0).value(2.0).endArray()
.startArray().value(103.0).value(3.0).endArray()
.endArray()
.endArray()
.endObject().string();
MultiLineString expected = GEOMETRY_FACTORY.createMultiLineString(new LineString[]{
GEOMETRY_FACTORY.createLineString(new Coordinate[]{
new Coordinate(100, 0),
new Coordinate(101, 1),
}),
GEOMETRY_FACTORY.createLineString(new Coordinate[]{
new Coordinate(102, 2),
new Coordinate(103, 3),
}),
});
assertGeometryEquals(jtsGeom(expected), multilinesGeoJson);
}
@Test
public void testParse_polygonNoHoles() throws IOException {
String polygonGeoJson = XContentFactory.jsonBuilder().startObject().field("type", "Polygon")
@ -227,6 +255,39 @@ public class GeoJSONShapeParserTests extends ElasticsearchTestCase {
assertGeometryEquals(expected, multiPolygonGeoJson);
}
@Test
public void testParse_geometryCollection() throws IOException {
String geometryCollectionGeoJson = XContentFactory.jsonBuilder().startObject()
.field("type","GeometryCollection")
.startArray("geometries")
.startObject()
.field("type", "LineString")
.startArray("coordinates")
.startArray().value(100.0).value(0.0).endArray()
.startArray().value(101.0).value(1.0).endArray()
.endArray()
.endObject()
.startObject()
.field("type", "Point")
.startArray("coordinates").value(102.0).value(2.0).endArray()
.endObject()
.endArray()
.endObject()
.string();
Shape[] expected = new Shape[2];
LineString expectedLineString = GEOMETRY_FACTORY.createLineString(new Coordinate[]{
new Coordinate(100, 0),
new Coordinate(101, 1),
});
expected[0] = jtsGeom(expectedLineString);
Point expectedPoint = GEOMETRY_FACTORY.createPoint(new Coordinate(102.0, 2.0));
expected[1] = new JtsPoint(expectedPoint, SPATIAL_CONTEXT);
//equals returns true only if geometries are in the same order
assertGeometryEquals(shapeCollection(expected), geometryCollectionGeoJson);
}
@Test
public void testThatParserExtractsCorrectTypeAndCoordinatesFromArbitraryJson() throws IOException {
String pointGeoJson = XContentFactory.jsonBuilder().startObject()

View File

@ -23,6 +23,7 @@ import org.elasticsearch.action.get.GetResponse;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.common.geo.ShapeRelation;
import org.elasticsearch.common.geo.builders.ShapeBuilder;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.index.query.FilterBuilders;
import org.elasticsearch.index.query.GeoShapeFilterBuilder;
@ -376,4 +377,55 @@ public class GeoShapeIntegrationTests extends ElasticsearchIntegrationTest {
assertThat(coordinates.get(1).get(1).doubleValue(), equalTo(-45.0));
assertThat(locationMap.size(), equalTo(2));
}
@Test
public void testShapeFilter_geometryCollection() throws Exception {
createIndex("shapes");
assertAcked(prepareCreate("test").addMapping("type", "location", "type=geo_shape"));
XContentBuilder docSource = jsonBuilder().startObject().startObject("location")
.field("type", "geometrycollection")
.startArray("geometries")
.startObject()
.field("type", "point")
.startArray("coordinates")
.value(100.0).value(0.0)
.endArray()
.endObject()
.startObject()
.field("type", "linestring")
.startArray("coordinates")
.startArray()
.value(101.0).value(0.0)
.endArray()
.startArray()
.value(102.0).value(1.0)
.endArray()
.endArray()
.endObject()
.endArray()
.endObject().endObject();
indexRandom(true,
client().prepareIndex("test", "type", "1")
.setSource(docSource));
ensureSearchable("test");
GeoShapeFilterBuilder filter = FilterBuilders.geoShapeFilter("location", ShapeBuilder.newGeometryCollection().polygon(ShapeBuilder.newPolygon().point(99.0, -1.0).point(99.0, 3.0).point(103.0, 3.0).point(103.0, -1.0).point(99.0, -1.0)), ShapeRelation.INTERSECTS);
SearchResponse result = client().prepareSearch("test").setQuery(QueryBuilders.matchAllQuery())
.setPostFilter(filter).get();
assertSearchResponse(result);
assertHitCount(result, 1);
filter = FilterBuilders.geoShapeFilter("location", ShapeBuilder.newGeometryCollection().polygon(ShapeBuilder.newPolygon().point(199.0, -11.0).point(199.0, 13.0).point(193.0, 13.0).point(193.0, -11.0).point(199.0, -11.0)), ShapeRelation.INTERSECTS);
result = client().prepareSearch("test").setQuery(QueryBuilders.matchAllQuery())
.setPostFilter(filter).get();
assertSearchResponse(result);
assertHitCount(result, 0);
filter = FilterBuilders.geoShapeFilter("location", ShapeBuilder.newGeometryCollection()
.polygon(ShapeBuilder.newPolygon().point(99.0, -1.0).point(99.0, 3.0).point(103.0, 3.0).point(103.0, -1.0).point(99.0, -1.0))
.polygon(ShapeBuilder.newPolygon().point(199.0, -11.0).point(199.0, 13.0).point(193.0, 13.0).point(193.0, -11.0).point(199.0, -11.0)), ShapeRelation.INTERSECTS);
result = client().prepareSearch("test").setQuery(QueryBuilders.matchAllQuery())
.setPostFilter(filter).get();
assertSearchResponse(result);
assertHitCount(result, 1);
}
}

View File

@ -19,7 +19,6 @@
package org.elasticsearch.test.hamcrest;
import com.carrotsearch.randomizedtesting.RandomizedTest;
import com.spatial4j.core.shape.Shape;
import com.spatial4j.core.shape.ShapeCollection;
import com.spatial4j.core.shape.jts.JtsGeometry;
@ -129,6 +128,10 @@ public class ElasticsearchGeoAssertions {
assertEquals(l1.getCoordinates(), l2.getCoordinates());
}
public static void assertEquals(MultiLineString l1, MultiLineString l2) {
assertEquals(l1.getCoordinates(), l2.getCoordinates());
}
public static void assertEquals(Polygon p1, Polygon p2) {
Assert.assertEquals(p1.getNumInteriorRing(), p2.getNumInteriorRing());
@ -166,6 +169,9 @@ public class ElasticsearchGeoAssertions {
} else if (s1 instanceof MultiPolygon && s2 instanceof MultiPolygon) {
assertEquals((MultiPolygon) s1, (MultiPolygon) s2);
} else if (s1 instanceof MultiLineString && s2 instanceof MultiLineString) {
assertEquals((MultiLineString) s1, (MultiLineString) s2);
} else {
throw new RuntimeException("equality of shape types not supported [" + s1.getClass().getName() + " and " + s2.getClass().getName() + "]");
}