Skip to content

Commit eb1ffaf

Browse files
colings86areek
authored andcommitted
Geo: Adds support for GeoJSON GeometryCollection
Closes #2796
1 parent b46bc4d commit eb1ffaf

File tree

6 files changed

+303
-5
lines changed

6 files changed

+303
-5
lines changed

docs/reference/mapping/types/geo-shape-type.asciidoc

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143,13 +143,13 @@ polygon and a minimum of `4` vertices.
143143
points.
144144
|`MultiLineString` |`multilinestring` |An array of separate linestrings.
145145
|`MultiPolygon` |`multipolygon` |An array of separate polygons.
146+
|`GeometryCollection` |`geometrycollection` | A GeoJSON shape similar to the
147+
`multi*` shapes except that multiple types can coexist (e.g., a Point
148+
and a LineString).
146149
|`N/A` |`envelope` |A bounding rectangle, or envelope, specified by
147150
specifying only the top left and bottom right points.
148151
|`N/A` |`circle` |A circle specified by a center point and radius with
149152
units, which default to `METERS`.
150-
|`GeometryCollection` |`N/A` | An unsupported GeoJSON shape similar to the
151-
`multi*` shapes except that multiple types can coexist (e.g., a Point
152-
and a LineString).
153153
|=======================================================================
154154

155155
[NOTE]
@@ -291,6 +291,31 @@ A list of geojson polygons.
291291
}
292292
--------------------------------------------------
293293

294+
[float]
295+
===== http://geojson.org/geojson-spec.html#geometrycollection[Geometry Collection]
296+
297+
A collection of geojson geometry objects.
298+
299+
[source,js]
300+
--------------------------------------------------
301+
{
302+
"location" : {
303+
"type": "geometrycollection",
304+
"geometries": [
305+
{
306+
"type": "point",
307+
"coordinates": [100.0, 0.0]
308+
},
309+
{
310+
"type": "linestring",
311+
"coordinates": [ [101.0, 0.0], [102.0, 1.0] ]
312+
}
313+
]
314+
}
315+
}
316+
--------------------------------------------------
317+
318+
294319
[float]
295320
===== Envelope
296321

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.elasticsearch.common.geo.builders;
21+
22+
import com.spatial4j.core.shape.Shape;
23+
import com.spatial4j.core.shape.ShapeCollection;
24+
import org.elasticsearch.common.xcontent.XContentBuilder;
25+
26+
import java.io.IOException;
27+
import java.util.ArrayList;
28+
import java.util.List;
29+
30+
public class GeometryCollectionBuilder extends ShapeBuilder {
31+
32+
public static final GeoShapeType TYPE = GeoShapeType.GEOMETRYCOLLECTION;
33+
34+
protected final ArrayList<ShapeBuilder> shapes = new ArrayList<>();
35+
36+
public GeometryCollectionBuilder shape(ShapeBuilder shape) {
37+
this.shapes.add(shape);
38+
return this;
39+
}
40+
41+
public GeometryCollectionBuilder point(PointBuilder point) {
42+
this.shapes.add(point);
43+
return this;
44+
}
45+
46+
public GeometryCollectionBuilder multiPoint(MultiPointBuilder multiPoint) {
47+
this.shapes.add(multiPoint);
48+
return this;
49+
}
50+
51+
public GeometryCollectionBuilder line(BaseLineStringBuilder<?> line) {
52+
this.shapes.add(line);
53+
return this;
54+
}
55+
56+
public GeometryCollectionBuilder multiLine(MultiLineStringBuilder multiLine) {
57+
this.shapes.add(multiLine);
58+
return this;
59+
}
60+
61+
public GeometryCollectionBuilder polygon(BasePolygonBuilder<?> polygon) {
62+
this.shapes.add(polygon);
63+
return this;
64+
}
65+
66+
public GeometryCollectionBuilder multiPolygon(MultiPolygonBuilder multiPolygon) {
67+
this.shapes.add(multiPolygon);
68+
return this;
69+
}
70+
71+
public GeometryCollectionBuilder envelope(EnvelopeBuilder envelope) {
72+
this.shapes.add(envelope);
73+
return this;
74+
}
75+
76+
public GeometryCollectionBuilder circle(CircleBuilder circle) {
77+
this.shapes.add(circle);
78+
return this;
79+
}
80+
81+
@Override
82+
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
83+
builder.startObject();
84+
builder.field(FIELD_TYPE, TYPE.shapename);
85+
builder.startArray(FIELD_GEOMETRIES);
86+
for (ShapeBuilder shape : shapes) {
87+
shape.toXContent(builder, params);
88+
}
89+
builder.endArray();
90+
return builder.endObject();
91+
}
92+
93+
@Override
94+
public GeoShapeType type() {
95+
return TYPE;
96+
}
97+
98+
@Override
99+
public Shape build() {
100+
101+
List<Shape> shapes = new ArrayList<>(this.shapes.size());
102+
103+
for (ShapeBuilder shape : this.shapes) {
104+
shapes.add(shape.build());
105+
}
106+
107+
if (shapes.size() == 1)
108+
return shapes.get(0);
109+
else
110+
return new ShapeCollection<>(shapes, SPATIAL_CONTEXT);
111+
//note: ShapeCollection is probably faster than a Multi* geom.
112+
}
113+
114+
}

src/main/java/org/elasticsearch/common/geo/builders/ShapeBuilder.java

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,14 @@ public static MultiPolygonBuilder newMultiPolygon() {
150150
return new MultiPolygonBuilder();
151151
}
152152

153+
/**
154+
* Create a new GeometryCollection
155+
* @return a new {@link GeometryCollectionBuilder}
156+
*/
157+
public static GeometryCollectionBuilder newGeometryCollection() {
158+
return new GeometryCollectionBuilder();
159+
}
160+
153161
/**
154162
* create a new Circle
155163
* @return a new {@link CircleBuilder}
@@ -498,6 +506,7 @@ public int compare(Edge o1, Edge o2) {
498506

499507
public static final String FIELD_TYPE = "type";
500508
public static final String FIELD_COORDINATES = "coordinates";
509+
public static final String FIELD_GEOMETRIES = "geometries";
501510

502511
protected static final boolean debugEnabled() {
503512
return LOGGER.isDebugEnabled() || DEBUG;
@@ -513,6 +522,7 @@ public static enum GeoShapeType {
513522
MULTILINESTRING("multilinestring"),
514523
POLYGON("polygon"),
515524
MULTIPOLYGON("multipolygon"),
525+
GEOMETRYCOLLECTION("geometrycollection"),
516526
ENVELOPE("envelope"),
517527
CIRCLE("circle");
518528

@@ -542,6 +552,7 @@ public static ShapeBuilder parse(XContentParser parser) throws IOException {
542552
GeoShapeType shapeType = null;
543553
Distance radius = null;
544554
CoordinateNode node = null;
555+
GeometryCollectionBuilder geometryCollections = null;
545556

546557
XContentParser.Token token;
547558
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
@@ -554,6 +565,9 @@ public static ShapeBuilder parse(XContentParser parser) throws IOException {
554565
} else if (FIELD_COORDINATES.equals(fieldName)) {
555566
parser.nextToken();
556567
node = parseCoordinates(parser);
568+
} else if (FIELD_GEOMETRIES.equals(fieldName)) {
569+
parser.nextToken();
570+
geometryCollections = parseGeometries(parser);
557571
} else if (CircleBuilder.FIELD_RADIUS.equals(fieldName)) {
558572
parser.nextToken();
559573
radius = Distance.parseDistance(parser.text());
@@ -566,8 +580,10 @@ public static ShapeBuilder parse(XContentParser parser) throws IOException {
566580

567581
if (shapeType == null) {
568582
throw new ElasticsearchParseException("Shape type not included");
569-
} else if (node == null) {
583+
} else if (node == null && GeoShapeType.GEOMETRYCOLLECTION != shapeType) {
570584
throw new ElasticsearchParseException("Coordinates not included");
585+
} else if (geometryCollections == null && GeoShapeType.GEOMETRYCOLLECTION == shapeType) {
586+
throw new ElasticsearchParseException("geometries not included");
571587
} else if (radius != null && GeoShapeType.CIRCLE != shapeType) {
572588
throw new ElasticsearchParseException("Field [" + CircleBuilder.FIELD_RADIUS + "] is supported for [" + CircleBuilder.TYPE
573589
+ "] only");
@@ -582,6 +598,7 @@ public static ShapeBuilder parse(XContentParser parser) throws IOException {
582598
case MULTIPOLYGON: return parseMultiPolygon(node);
583599
case CIRCLE: return parseCircle(node, radius);
584600
case ENVELOPE: return parseEnvelope(node);
601+
case GEOMETRYCOLLECTION: return geometryCollections;
585602
default:
586603
throw new ElasticsearchParseException("Shape type [" + shapeType + "] not included");
587604
}
@@ -639,5 +656,28 @@ protected static MultiPolygonBuilder parseMultiPolygon(CoordinateNode coordinate
639656
}
640657
return polygons;
641658
}
659+
660+
/**
661+
* Parse the geometries array of a GeometryCollection
662+
*
663+
* @param parser Parser that will be read from
664+
* @return Geometry[] geometries of the GeometryCollection
665+
* @throws IOException Thrown if an error occurs while reading from the XContentParser
666+
*/
667+
protected static GeometryCollectionBuilder parseGeometries(XContentParser parser) throws IOException {
668+
if (parser.currentToken() != XContentParser.Token.START_ARRAY) {
669+
throw new ElasticsearchParseException("Geometries must be an array of geojson objects");
670+
}
671+
672+
XContentParser.Token token = parser.nextToken();
673+
GeometryCollectionBuilder geometryCollection = newGeometryCollection();
674+
while (token != XContentParser.Token.END_ARRAY) {
675+
ShapeBuilder shapeBuilder = GeoShapeType.parse(parser);
676+
geometryCollection.shape(shapeBuilder);
677+
token = parser.nextToken();
678+
}
679+
680+
return geometryCollection;
681+
}
642682
}
643683
}

src/test/java/org/elasticsearch/common/geo/GeoJSONShapeParserTests.java

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,34 @@ public void testParse_lineString() throws IOException {
7575
assertGeometryEquals(jtsGeom(expected), lineGeoJson);
7676
}
7777

78+
@Test
79+
public void testParse_multiLineString() throws IOException {
80+
String multilinesGeoJson = XContentFactory.jsonBuilder().startObject().field("type", "MultiLineString")
81+
.startArray("coordinates")
82+
.startArray()
83+
.startArray().value(100.0).value(0.0).endArray()
84+
.startArray().value(101.0).value(1.0).endArray()
85+
.endArray()
86+
.startArray()
87+
.startArray().value(102.0).value(2.0).endArray()
88+
.startArray().value(103.0).value(3.0).endArray()
89+
.endArray()
90+
.endArray()
91+
.endObject().string();
92+
93+
MultiLineString expected = GEOMETRY_FACTORY.createMultiLineString(new LineString[]{
94+
GEOMETRY_FACTORY.createLineString(new Coordinate[]{
95+
new Coordinate(100, 0),
96+
new Coordinate(101, 1),
97+
}),
98+
GEOMETRY_FACTORY.createLineString(new Coordinate[]{
99+
new Coordinate(102, 2),
100+
new Coordinate(103, 3),
101+
}),
102+
});
103+
assertGeometryEquals(jtsGeom(expected), multilinesGeoJson);
104+
}
105+
78106
@Test
79107
public void testParse_polygonNoHoles() throws IOException {
80108
String polygonGeoJson = XContentFactory.jsonBuilder().startObject().field("type", "Polygon")
@@ -227,6 +255,39 @@ public void testParse_multiPolygon() throws IOException {
227255
assertGeometryEquals(expected, multiPolygonGeoJson);
228256
}
229257

258+
@Test
259+
public void testParse_geometryCollection() throws IOException {
260+
String geometryCollectionGeoJson = XContentFactory.jsonBuilder().startObject()
261+
.field("type","GeometryCollection")
262+
.startArray("geometries")
263+
.startObject()
264+
.field("type", "LineString")
265+
.startArray("coordinates")
266+
.startArray().value(100.0).value(0.0).endArray()
267+
.startArray().value(101.0).value(1.0).endArray()
268+
.endArray()
269+
.endObject()
270+
.startObject()
271+
.field("type", "Point")
272+
.startArray("coordinates").value(102.0).value(2.0).endArray()
273+
.endObject()
274+
.endArray()
275+
.endObject()
276+
.string();
277+
278+
Shape[] expected = new Shape[2];
279+
LineString expectedLineString = GEOMETRY_FACTORY.createLineString(new Coordinate[]{
280+
new Coordinate(100, 0),
281+
new Coordinate(101, 1),
282+
});
283+
expected[0] = jtsGeom(expectedLineString);
284+
Point expectedPoint = GEOMETRY_FACTORY.createPoint(new Coordinate(102.0, 2.0));
285+
expected[1] = new JtsPoint(expectedPoint, SPATIAL_CONTEXT);
286+
287+
//equals returns true only if geometries are in the same order
288+
assertGeometryEquals(shapeCollection(expected), geometryCollectionGeoJson);
289+
}
290+
230291
@Test
231292
public void testThatParserExtractsCorrectTypeAndCoordinatesFromArbitraryJson() throws IOException {
232293
String pointGeoJson = XContentFactory.jsonBuilder().startObject()

0 commit comments

Comments
 (0)