Skip to content

Commit

Permalink
Geo: Adds support for GeoJSON GeometryCollection
Browse files Browse the repository at this point in the history
Closes #2796
  • Loading branch information
colings86 authored and areek committed Sep 8, 2014
1 parent b46bc4d commit eb1ffaf
Show file tree
Hide file tree
Showing 6 changed files with 303 additions and 5 deletions.
31 changes: 28 additions & 3 deletions docs/reference/mapping/types/geo-shape-type.asciidoc
Expand Up @@ -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]
Expand Down Expand Up @@ -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

Expand Down
@@ -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.
}

}
Expand Up @@ -150,6 +150,14 @@ public static MultiPolygonBuilder newMultiPolygon() {
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}
Expand Down Expand Up @@ -498,6 +506,7 @@ public int compare(Edge o1, Edge o2) {

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;
Expand All @@ -513,6 +522,7 @@ public static enum GeoShapeType {
MULTILINESTRING("multilinestring"),
POLYGON("polygon"),
MULTIPOLYGON("multipolygon"),
GEOMETRYCOLLECTION("geometrycollection"),
ENVELOPE("envelope"),
CIRCLE("circle");

Expand Down Expand Up @@ -542,6 +552,7 @@ public static ShapeBuilder parse(XContentParser parser) throws IOException {
GeoShapeType shapeType = null;
Distance radius = null;
CoordinateNode node = null;
GeometryCollectionBuilder geometryCollections = null;

XContentParser.Token token;
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
Expand All @@ -554,6 +565,9 @@ public static ShapeBuilder parse(XContentParser parser) throws IOException {
} 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());
Expand All @@ -566,8 +580,10 @@ public static ShapeBuilder parse(XContentParser parser) throws IOException {

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");
Expand All @@ -582,6 +598,7 @@ public static ShapeBuilder parse(XContentParser parser) throws IOException {
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");
}
Expand Down Expand Up @@ -639,5 +656,28 @@ protected static MultiPolygonBuilder parseMultiPolygon(CoordinateNode coordinate
}
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;
}
}
}
Expand Up @@ -75,6 +75,34 @@ public void testParse_lineString() throws IOException {
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")
Expand Down Expand Up @@ -227,6 +255,39 @@ public void testParse_multiPolygon() throws IOException {
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()
Expand Down

0 comments on commit eb1ffaf

Please sign in to comment.