Skip to content

Commit

Permalink
[GEO] Add LinearRing and LineString validity checks as defined by htt…
Browse files Browse the repository at this point in the history
…p://geojson.org/geojson-spec.html to ensure valid polygons are specified at parse time.

Closes #8433
  • Loading branch information
nknize committed Nov 19, 2014
1 parent e7c8491 commit 6b91955
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -206,13 +206,17 @@ public String toString() {
private static CoordinateNode parseCoordinates(XContentParser parser) throws IOException {
XContentParser.Token token = parser.nextToken();

// Base case
if (token != XContentParser.Token.START_ARRAY) {
// Base cases
if (token != XContentParser.Token.START_ARRAY &&
token != XContentParser.Token.END_ARRAY &&
token != XContentParser.Token.VALUE_NULL) {
double lon = parser.doubleValue();
token = parser.nextToken();
double lat = parser.doubleValue();
token = parser.nextToken();
return new CoordinateNode(new Coordinate(lon, lat));
} else if (token == XContentParser.Token.VALUE_NULL) {
throw new ElasticsearchIllegalArgumentException("coordinates cannot contain NULL values)");
}

List<CoordinateNode> nodes = new ArrayList<>();
Expand Down Expand Up @@ -625,6 +629,16 @@ protected static MultiPointBuilder parseMultiPoint(CoordinateNode coordinates) {
}

protected static LineStringBuilder parseLineString(CoordinateNode coordinates) {
/**
* Per GeoJSON spec (http://geojson.org/geojson-spec.html#linestring)
* "coordinates" member must be an array of two or more positions
* LineStringBuilder should throw a graceful exception if < 2 coordinates/points are provided
*/
if (coordinates.children.size() < 2) {
throw new ElasticsearchParseException("Invalid number of points in LineString (found " +
coordinates.children.size() + " - must be >= 2)");
}

LineStringBuilder line = newLineString();
for (CoordinateNode node : coordinates.children) {
line.point(node.coordinate);
Expand All @@ -640,11 +654,28 @@ protected static MultiLineStringBuilder parseMultiLine(CoordinateNode coordinate
return multiline;
}

protected static LineStringBuilder parseLinearRing(CoordinateNode coordinates) {
/**
* Per GeoJSON spec (http://geojson.org/geojson-spec.html#linestring)
* A LinearRing is closed LineString with 4 or more positions. The first and last positions
* are equivalent (they represent equivalent points). Though a LinearRing is not explicitly
* represented as a GeoJSON geometry type, it is referred to in the Polygon geometry type definition.
*/
if (coordinates.children.size() < 4) {
throw new ElasticsearchParseException("Invalid number of points in LinearRing (found " +
coordinates.children.size() + " - must be >= 4)");
} else if (!coordinates.children.get(0).coordinate.equals(
coordinates.children.get(coordinates.children.size() - 1).coordinate)) {
throw new ElasticsearchParseException("Invalid LinearRing found (coordinates are not closed)");
}
return parseLineString(coordinates);
}

protected static PolygonBuilder parsePolygon(CoordinateNode coordinates) {
LineStringBuilder shell = parseLineString(coordinates.children.get(0));
LineStringBuilder shell = parseLinearRing(coordinates.children.get(0));
PolygonBuilder polygon = new PolygonBuilder(shell.points);
for (int i = 1; i < coordinates.children.size(); i++) {
polygon.hole(parseLineString(coordinates.children.get(i)));
polygon.hole(parseLinearRing(coordinates.children.get(i)));
}
return polygon;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
import com.spatial4j.core.shape.jts.JtsGeometry;
import com.spatial4j.core.shape.jts.JtsPoint;
import com.vividsolutions.jts.geom.*;
import org.elasticsearch.ElasticsearchIllegalArgumentException;
import org.elasticsearch.ElasticsearchParseException;
import org.elasticsearch.common.geo.builders.ShapeBuilder;
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.common.xcontent.XContentParser;
Expand Down Expand Up @@ -155,6 +157,76 @@ public void testParse_polygonNoHoles() throws IOException {
assertGeometryEquals(jtsGeom(expected), polygonGeoJson);
}

@Test
public void testParse_invalidPolygon() throws IOException {
/**
* The following 3 test cases ensure proper error handling of invalid polygons
* per the GeoJSON specification
*/
// test case 1: create an invalid polygon with only 2 points
String invalidPoly1 = XContentFactory.jsonBuilder().startObject().field("type", "polygon")
.startArray("coordinates")
.startArray()
.startArray().value(-74.011).value(40.753).endArray()
.startArray().value(-75.022).value(41.783).endArray()
.endArray()
.endArray()
.endObject().string();
XContentParser parser = JsonXContent.jsonXContent.createParser(invalidPoly1);
parser.nextToken();
ElasticsearchGeoAssertions.assertValidException(parser, ElasticsearchParseException.class);

// test case 2: create an invalid polygon with only 1 point
String invalidPoly2 = XContentFactory.jsonBuilder().startObject().field("type", "polygon")
.startArray("coordinates")
.startArray()
.startArray().value(-74.011).value(40.753).endArray()
.endArray()
.endArray()
.endObject().string();

parser = JsonXContent.jsonXContent.createParser(invalidPoly2);
parser.nextToken();
ElasticsearchGeoAssertions.assertValidException(parser, ElasticsearchParseException.class);

// test case 3: create an invalid polygon with 0 points
String invalidPoly3 = XContentFactory.jsonBuilder().startObject().field("type", "polygon")
.startArray("coordinates")
.startArray()
.startArray().endArray()
.endArray()
.endArray()
.endObject().string();

parser = JsonXContent.jsonXContent.createParser(invalidPoly3);
parser.nextToken();
ElasticsearchGeoAssertions.assertValidException(parser, ElasticsearchParseException.class);

// test case 4: create an invalid polygon with null value points
String invalidPoly4 = XContentFactory.jsonBuilder().startObject().field("type", "polygon")
.startArray("coordinates")
.startArray()
.startArray().nullValue().nullValue().endArray()
.endArray()
.endArray()
.endObject().string();

parser = JsonXContent.jsonXContent.createParser(invalidPoly4);
parser.nextToken();
ElasticsearchGeoAssertions.assertValidException(parser, ElasticsearchIllegalArgumentException.class);

// test case 5: create an invalid polygon with 1 invalid LinearRing
String invalidPoly5 = XContentFactory.jsonBuilder().startObject().field("type", "polygon")
.startArray("coordinates")
.nullValue().nullValue()
.endArray()
.endObject().string();

parser = JsonXContent.jsonXContent.createParser(invalidPoly5);
parser.nextToken();
ElasticsearchGeoAssertions.assertValidException(parser, ElasticsearchIllegalArgumentException.class);
}

@Test
public void testParse_polygonWithHole() throws IOException {
String polygonGeoJson = XContentFactory.jsonBuilder().startObject().field("type", "Polygon")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,12 @@
import com.spatial4j.core.shape.jts.JtsGeometry;
import com.spatial4j.core.shape.jts.JtsPoint;
import com.vividsolutions.jts.geom.*;
import org.elasticsearch.ElasticsearchParseException;
import org.elasticsearch.common.geo.GeoDistance;
import org.elasticsearch.common.geo.GeoPoint;
import org.elasticsearch.common.geo.builders.ShapeBuilder;
import org.elasticsearch.common.unit.DistanceUnit;
import org.elasticsearch.common.xcontent.XContentParser;
import org.hamcrest.Matcher;
import org.junit.Assert;

Expand Down Expand Up @@ -246,4 +249,13 @@ private static double distance(double lat1, double lon1, double lat2, double lon
return GeoDistance.ARC.calculate(lat1, lon1, lat2, lon2, DistanceUnit.DEFAULT);
}

public static void assertValidException(XContentParser parser, Class expectedException) {
try {
ShapeBuilder.parse(parser);
Assert.fail("process completed successfully when " + expectedException.getName() + " expected");
} catch (Exception e) {
assert(e.getClass().equals(expectedException)):
"expected " + expectedException.getName() + " but found " + e.getClass().getName();
}
}
}

0 comments on commit 6b91955

Please sign in to comment.