Browse Source

Geo: Adds support for GeoJSON GeometryCollection

Closes #2796
Colin Goodheart-Smithe 11 years ago
parent
commit
b2286915cd

+ 28 - 3
docs/reference/mapping/types/geo-shape-type.asciidoc

@@ -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
 

+ 114 - 0
src/main/java/org/elasticsearch/common/geo/builders/GeometryCollectionBuilder.java

@@ -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.
+    }
+
+}

+ 41 - 1
src/main/java/org/elasticsearch/common/geo/builders/ShapeBuilder.java

@@ -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;
+        }
     }
 }

+ 61 - 0
src/test/java/org/elasticsearch/common/geo/GeoJSONShapeParserTests.java

@@ -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()

+ 52 - 0
src/test/java/org/elasticsearch/search/geo/GeoShapeIntegrationTests.java

@@ -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);
+    }
 }

+ 7 - 1
src/test/java/org/elasticsearch/test/hamcrest/ElasticsearchGeoAssertions.java

@@ -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() + "]");
         }