diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt
index f3da37c6f22..f0bee656a8e 100644
--- a/lucene/CHANGES.txt
+++ b/lucene/CHANGES.txt
@@ -29,6 +29,10 @@ New Features
* LUCENE-7355: Added Analyzer#normalize(), which only applies normalization to
an input string. (Adrien Grand)
+* LUCENE-7380: Add Polygon.fromGeoJSON for more easily creating
+ Polygon instances from a standard GeoJSON string (Robert Muir, Mike
+ McCandless)
+
Bug Fixes
* LUCENE-6662: Fixed potential resource leaks. (Rishabh Patel via Adrien Grand)
diff --git a/lucene/core/src/java/org/apache/lucene/geo/Polygon.java b/lucene/core/src/java/org/apache/lucene/geo/Polygon.java
index 3b5dec9ca2d..99453b9fb44 100644
--- a/lucene/core/src/java/org/apache/lucene/geo/Polygon.java
+++ b/lucene/core/src/java/org/apache/lucene/geo/Polygon.java
@@ -16,10 +16,13 @@
*/
package org.apache.lucene.geo;
+import java.text.ParseException;
import java.util.Arrays;
/**
- * Represents a closed polygon on the earth's surface.
+ * Represents a closed polygon on the earth's surface. You can either construct the Polygon directly yourself with {@code double[]}
+ * coordinates, or use {@link Polygon#fromGeoJSON} if you have a polygon already encoded as a
+ * GeoJSON string.
*
@@ -159,4 +162,12 @@ public final class Polygon {
}
return sb.toString();
}
+
+ /** Parses a standard GeoJSON polygon string. The type of the incoming GeoJSON object must be a Polygon or MultiPolygon, optionally
+ * embedded under a "type: Feature". A Polygon will return as a length 1 array, while a MultiPolygon will be 1 or more in length.
+ *
+ * See the GeoJSON specification . */
+ public static Polygon[] fromGeoJSON(String geojson) throws ParseException {
+ return new SimpleGeoJSONPolygonParser(geojson).parse();
+ }
}
diff --git a/lucene/core/src/java/org/apache/lucene/geo/SimpleGeoJSONPolygonParser.java b/lucene/core/src/java/org/apache/lucene/geo/SimpleGeoJSONPolygonParser.java
new file mode 100644
index 00000000000..278307f512e
--- /dev/null
+++ b/lucene/core/src/java/org/apache/lucene/geo/SimpleGeoJSONPolygonParser.java
@@ -0,0 +1,440 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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.apache.lucene.geo;
+
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.List;
+
+/*
+ We accept either a whole type: Feature, like this:
+
+ { "type": "Feature",
+ "geometry": {
+ "type": "Polygon",
+ "coordinates": [
+ [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0],
+ [100.0, 1.0], [100.0, 0.0] ]
+ ]
+ },
+ "properties": {
+ "prop0": "value0",
+ "prop1": {"this": "that"}
+ }
+ }
+
+ Or the inner object with type: Multi/Polygon.
+
+ Or a type: FeatureCollection, if it has only one Feature which is a Polygon or MultiPolyon.
+
+ type: MultiPolygon (union of polygons) is also accepted.
+*/
+
+/** Does minimal parsing of a GeoJSON object, to extract either Polygon or MultiPolygon, either directly as a the top-level type, or if
+ * the top-level type is Feature, as the geometry of that feature. */
+
+@SuppressWarnings("unchecked")
+class SimpleGeoJSONPolygonParser {
+ final String input;
+ private int upto;
+ private String polyType;
+ private List coordinates;
+
+ public SimpleGeoJSONPolygonParser(String input) {
+ this.input = input;
+ }
+
+ public Polygon[] parse() throws ParseException {
+ // parse entire object
+ parseObject("");
+
+ // make sure there's nothing left:
+ readEnd();
+
+ // The order of JSON object keys (type, geometry, coordinates in our case) can be arbitrary, so we wait until we are done parsing to
+ // put the pieces together here:
+
+ if (coordinates == null) {
+ throw newParseException("did not see any polygon coordinates");
+ }
+
+ if (polyType == null) {
+ throw newParseException("did not see type: Polygon or MultiPolygon");
+ }
+
+ if (polyType.equals("Polygon")) {
+ return new Polygon[] {parsePolygon(coordinates)};
+ } else {
+ List polygons = new ArrayList<>();
+ for(int i=0;i) o));
+ }
+
+ return polygons.toArray(new Polygon[polygons.size()]);
+ }
+ }
+
+ /** path is the "address" by keys of where we are, e.g. geometry.coordinates */
+ private void parseObject(String path) throws ParseException {
+ scan('{');
+ boolean first = true;
+ while (true) {
+ char ch = peek();
+ if (ch == '}') {
+ break;
+ } else if (first == false) {
+ if (ch == ',') {
+ // ok
+ upto++;
+ ch = peek();
+ if (ch == '}') {
+ break;
+ }
+ } else {
+ throw newParseException("expected , but got " + ch);
+ }
+ }
+
+ first = false;
+
+ int uptoStart = upto;
+ String key = parseString();
+
+ if (path.equals("crs.properties") && key.equals("href")) {
+ upto = uptoStart;
+ throw newParseException("cannot handle linked crs");
+ }
+
+ scan(':');
+
+ Object o;
+
+ ch = peek();
+
+ uptoStart = upto;
+
+ if (ch == '[') {
+ String newPath;
+ if (path.length() == 0) {
+ newPath = key;
+ } else {
+ newPath = path + "." + key;
+ }
+ o = parseArray(newPath);
+ } else if (ch == '{') {
+ String newPath;
+ if (path.length() == 0) {
+ newPath = key;
+ } else {
+ newPath = path + "." + key;
+ }
+ parseObject(newPath);
+ o = null;
+ } else if (ch == '"') {
+ o = parseString();
+ } else if (ch == 't') {
+ scan("true");
+ o = Boolean.TRUE;
+ } else if (ch == 'f') {
+ scan("false");
+ o = Boolean.FALSE;
+ } else if (ch == 'n') {
+ scan("null");
+ o = null;
+ } else if (ch == '-' || ch == '.' || (ch >= '0' && ch <= '9')) {
+ o = parseNumber();
+ } else if (ch == '}') {
+ break;
+ } else {
+ throw newParseException("expected array, object, string or literal value, but got: " + ch);
+ }
+
+ if (path.equals("crs.properties") && key.equals("name")) {
+ if (o instanceof String == false) {
+ upto = uptoStart;
+ throw newParseException("crs.properties.name should be a string, but saw: " + o);
+ }
+ String crs = (String) o;
+ if (crs.startsWith("urn:ogc:def:crs:OGC") == false || crs.endsWith(":CRS84") == false) {
+ upto = uptoStart;
+ throw newParseException("crs must be CRS84 from OGC, but saw: " + o);
+ }
+ }
+
+ if (key.equals("type") && path.startsWith("crs") == false) {
+ if (o instanceof String == false) {
+ upto = uptoStart;
+ throw newParseException("type should be a string, but got: " + o);
+ }
+ String type = (String) o;
+ if (type.equals("Polygon") && isValidGeometryPath(path)) {
+ polyType = "Polygon";
+ } else if (type.equals("MultiPolygon") && isValidGeometryPath(path)) {
+ polyType = "MultiPolygon";
+ } else if ((type.equals("FeatureCollection") || type.equals("Feature")) && (path.equals("features.[]") || path.equals(""))) {
+ // OK, we recurse
+ } else {
+ upto = uptoStart;
+ throw newParseException("can only handle type FeatureCollection (if it has a single polygon geometry), Feature, Polygon or MutiPolygon, but got " + type);
+ }
+ } else if (key.equals("coordinates") && isValidGeometryPath(path)) {
+ if (o instanceof List == false) {
+ upto = uptoStart;
+ throw newParseException("coordinates should be an array, but got: " + o.getClass());
+ }
+ if (coordinates != null) {
+ upto = uptoStart;
+ throw newParseException("only one Polygon or MultiPolygon is supported");
+ }
+ coordinates = (List) o;
+ }
+ }
+
+ scan('}');
+ }
+
+ /** Returns true if the object path is a valid location to see a Multi/Polygon geometry */
+ private boolean isValidGeometryPath(String path) {
+ return path.equals("") || path.equals("geometry") || path.equals("features.[].geometry");
+ }
+
+ private Polygon parsePolygon(List coordinates) throws ParseException {
+ List holes = new ArrayList<>();
+ Object o = coordinates.get(0);
+ if (o instanceof List == false) {
+ throw newParseException("first element of polygon array must be an array [[lat, lon], [lat, lon] ...] but got: " + o);
+ }
+ double[][] polyPoints = parsePoints((List) o);
+ for(int i=1;i) o);
+ holes.add(new Polygon(holePoints[0], holePoints[1]));
+ }
+ return new Polygon(polyPoints[0], polyPoints[1], holes.toArray(new Polygon[holes.size()]));
+ }
+
+ /** Parses [[lat, lon], [lat, lon] ...] into 2d double array */
+ private double[][] parsePoints(List o) throws ParseException {
+ double[] lats = new double[o.size()];
+ double[] lons = new double[o.size()];
+ for(int i=0;i pointList = (List) point;
+ if (pointList.size() != 2) {
+ throw newParseException("elements of coordinates array must [lat, lon] array, but got wrong element count: " + pointList);
+ }
+ if (pointList.get(0) instanceof Double == false) {
+ throw newParseException("elements of coordinates array must [lat, lon] array, but first element is not a Double: " + pointList.get(0));
+ }
+ if (pointList.get(1) instanceof Double == false) {
+ throw newParseException("elements of coordinates array must [lat, lon] array, but second element is not a Double: " + pointList.get(1));
+ }
+
+ // lon, lat ordering in GeoJSON!
+ lons[i] = ((Double) pointList.get(0)).doubleValue();
+ lats[i] = ((Double) pointList.get(1)).doubleValue();
+ }
+
+ return new double[][] {lats, lons};
+ }
+
+ private List parseArray(String path) throws ParseException {
+ List result = new ArrayList<>();
+ scan('[');
+ while (upto < input.length()) {
+ char ch = peek();
+ if (ch == ']') {
+ scan(']');
+ return result;
+ }
+
+ if (result.size() > 0) {
+ if (ch != ',') {
+ throw newParseException("expected ',' separating list items, but got '" + ch + "'");
+ }
+
+ // skip the ,
+ upto++;
+
+ if (upto == input.length()) {
+ throw newParseException("hit EOF while parsing array");
+ }
+ ch = peek();
+ }
+
+ Object o;
+ if (ch == '[') {
+ o = parseArray(path + ".[]");
+ } else if (ch == '{') {
+ // This is only used when parsing the "features" in type: FeatureCollection
+ parseObject(path + ".[]");
+ o = null;
+ } else if (ch == '-' || ch == '.' || (ch >= '0' && ch <= '9')) {
+ o = parseNumber();
+ } else {
+ throw newParseException("expected another array or number while parsing array, not '" + ch + "'");
+ }
+
+ result.add(o);
+ }
+
+ throw newParseException("hit EOF while reading array");
+ }
+
+ private Number parseNumber() throws ParseException {
+ StringBuilder b = new StringBuilder();
+ int uptoStart = upto;
+ while (upto < input.length()) {
+ char ch = input.charAt(upto);
+ if (ch == '-' || ch == '.' || (ch >= '0' && ch <= '9') || ch == 'e' || ch == 'E') {
+ upto++;
+ b.append(ch);
+ } else {
+ break;
+ }
+ }
+
+ // we only handle doubles
+ try {
+ return Double.parseDouble(b.toString());
+ } catch (NumberFormatException nfe) {
+ upto = uptoStart;
+ throw newParseException("could not parse number as double");
+ }
+ }
+
+ private String parseString() throws ParseException {
+ scan('"');
+ StringBuilder b = new StringBuilder();
+ while (upto < input.length()) {
+ char ch = input.charAt(upto);
+ if (ch == '"') {
+ upto++;
+ return b.toString();
+ }
+ if (ch == '\\') {
+ // an escaped character
+ upto++;
+ if (upto == input.length()) {
+ throw newParseException("hit EOF inside string literal");
+ }
+ ch = input.charAt(upto);
+ if (ch == 'u') {
+ // 4 hex digit unicode BMP escape
+ upto++;
+ if (upto + 4 > input.length()) {
+ throw newParseException("hit EOF inside string literal");
+ }
+ b.append(Integer.parseInt(input.substring(upto, upto+4), 16));
+ } else if (ch == '\\') {
+ b.append('\\');
+ upto++;
+ } else {
+ // TODO: allow \n, \t, etc.???
+ throw newParseException("unsupported string escape character \\" + ch);
+ }
+ } else {
+ b.append(ch);
+ upto++;
+ }
+ }
+
+ throw newParseException("hit EOF inside string literal");
+ }
+
+ private char peek() throws ParseException {
+ while (upto < input.length()) {
+ char ch = input.charAt(upto);
+ if (isJSONWhitespace(ch)) {
+ upto++;
+ continue;
+ }
+ return ch;
+ }
+
+ throw newParseException("unexpected EOF");
+ }
+
+ /** Scans across whitespace and consumes the expected character, or throws {@code ParseException} if the character is wrong */
+ private void scan(char expected) throws ParseException {
+ while (upto < input.length()) {
+ char ch = input.charAt(upto);
+ if (isJSONWhitespace(ch)) {
+ upto++;
+ continue;
+ }
+ if (ch != expected) {
+ throw newParseException("expected '" + expected + "' but got '" + ch + "'");
+ }
+ upto++;
+ return;
+ }
+ throw newParseException("expected '" + expected + "' but got EOF");
+ }
+
+ private void readEnd() throws ParseException {
+ while (upto < input.length()) {
+ char ch = input.charAt(upto);
+ if (isJSONWhitespace(ch) == false) {
+ throw newParseException("unexpected character '" + ch + "' after end of GeoJSON object");
+ }
+ upto++;
+ }
+ }
+
+ /** Scans the expected string, or throws {@code ParseException} */
+ private void scan(String expected) throws ParseException {
+ if (upto + expected.length() > input.length()) {
+ throw newParseException("expected \"" + expected + "\" but hit EOF");
+ }
+ String subString = input.substring(upto, upto+expected.length());
+ if (subString.equals(expected) == false) {
+ throw newParseException("expected \"" + expected + "\" but got \"" + subString + "\"");
+ }
+ upto += expected.length();
+ }
+
+ private static boolean isJSONWhitespace(char ch) {
+ // JSON doesn't accept allow unicode whitespace?
+ return ch == 0x20 || // space
+ ch == 0x09 || // tab
+ ch == 0x0a || // line feed
+ ch == 0x0d; // newline
+ }
+
+ /** When calling this, upto should be at the position of the incorrect character! */
+ private ParseException newParseException(String details) throws ParseException {
+ String fragment;
+ int end = Math.min(input.length(), upto+1);
+ if (upto < 50) {
+ fragment = input.substring(0, end);
+ } else {
+ fragment = "..." + input.substring(upto-50, end);
+ }
+ return new ParseException(details + " at character offset " + upto + "; fragment leading to this:\n" + fragment, upto);
+ }
+}
diff --git a/lucene/core/src/test/org/apache/lucene/geo/TestPolygon.java b/lucene/core/src/test/org/apache/lucene/geo/TestPolygon.java
index 401092f5d50..8ee62718e6d 100644
--- a/lucene/core/src/test/org/apache/lucene/geo/TestPolygon.java
+++ b/lucene/core/src/test/org/apache/lucene/geo/TestPolygon.java
@@ -16,6 +16,8 @@
*/
package org.apache.lucene.geo;
+import java.text.ParseException;
+
import org.apache.lucene.util.LuceneTestCase;
public class TestPolygon extends LuceneTestCase {
@@ -59,4 +61,243 @@ public class TestPolygon extends LuceneTestCase {
});
assertTrue(expected.getMessage(), expected.getMessage().contains("it must close itself"));
}
+
+ public void testGeoJSONPolygon() throws Exception {
+ StringBuilder b = new StringBuilder();
+ b.append("{\n");
+ b.append(" \"type\": \"Polygon\",\n");
+ b.append(" \"coordinates\": [\n");
+ b.append(" [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0],\n");
+ b.append(" [100.0, 1.0], [100.0, 0.0] ]\n");
+ b.append(" ]\n");
+ b.append("}\n");
+
+ Polygon[] polygons = Polygon.fromGeoJSON(b.toString());
+ assertEquals(1, polygons.length);
+ assertEquals(new Polygon(new double[] {0.0, 0.0, 1.0, 1.0, 0.0},
+ new double[] {100.0, 101.0, 101.0, 100.0, 100.0}), polygons[0]);
+ }
+
+ public void testGeoJSONPolygonWithHole() throws Exception {
+ StringBuilder b = new StringBuilder();
+ b.append("{\n");
+ b.append(" \"type\": \"Polygon\",\n");
+ b.append(" \"coordinates\": [\n");
+ b.append(" [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0],\n");
+ b.append(" [100.0, 1.0], [100.0, 0.0] ],\n");
+ b.append(" [ [100.5, 0.5], [100.5, 0.75], [100.75, 0.75], [100.75, 0.5], [100.5, 0.5]]\n");
+ b.append(" ]\n");
+ b.append("}\n");
+
+ Polygon hole = new Polygon(new double[] {0.5, 0.75, 0.75, 0.5, 0.5},
+ new double[] {100.5, 100.5, 100.75, 100.75, 100.5});
+ Polygon expected = new Polygon(new double[] {0.0, 0.0, 1.0, 1.0, 0.0},
+ new double[] {100.0, 101.0, 101.0, 100.0, 100.0}, hole);
+ Polygon[] polygons = Polygon.fromGeoJSON(b.toString());
+
+ assertEquals(1, polygons.length);
+ assertEquals(expected, polygons[0]);
+ }
+
+ // a MultiPolygon returns multiple Polygons
+ public void testGeoJSONMultiPolygon() throws Exception {
+ StringBuilder b = new StringBuilder();
+ b.append("{\n");
+ b.append(" \"type\": \"MultiPolygon\",\n");
+ b.append(" \"coordinates\": [\n");
+ b.append(" [\n");
+ b.append(" [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0],\n");
+ b.append(" [100.0, 1.0], [100.0, 0.0] ]\n");
+ b.append(" ],\n");
+ b.append(" [\n");
+ b.append(" [ [10.0, 2.0], [11.0, 2.0], [11.0, 3.0],\n");
+ b.append(" [10.0, 3.0], [10.0, 2.0] ]\n");
+ b.append(" ]\n");
+ b.append(" ],\n");
+ b.append("}\n");
+
+ Polygon[] polygons = Polygon.fromGeoJSON(b.toString());
+ assertEquals(2, polygons.length);
+ assertEquals(new Polygon(new double[] {0.0, 0.0, 1.0, 1.0, 0.0},
+ new double[] {100.0, 101.0, 101.0, 100.0, 100.0}), polygons[0]);
+ assertEquals(new Polygon(new double[] {2.0, 2.0, 3.0, 3.0, 2.0},
+ new double[] {10.0, 11.0, 11.0, 10.0, 10.0}), polygons[1]);
+ }
+
+ // make sure type can appear last (JSON allows arbitrary key/value order for objects)
+ public void testGeoJSONTypeComesLast() throws Exception {
+ StringBuilder b = new StringBuilder();
+ b.append("{\n");
+ b.append(" \"coordinates\": [\n");
+ b.append(" [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0],\n");
+ b.append(" [100.0, 1.0], [100.0, 0.0] ]\n");
+ b.append(" ],\n");
+ b.append(" \"type\": \"Polygon\",\n");
+ b.append("}\n");
+
+ Polygon[] polygons = Polygon.fromGeoJSON(b.toString());
+ assertEquals(1, polygons.length);
+ assertEquals(new Polygon(new double[] {0.0, 0.0, 1.0, 1.0, 0.0},
+ new double[] {100.0, 101.0, 101.0, 100.0, 100.0}), polygons[0]);
+ }
+
+ // make sure Polygon inside a type: Feature also works
+ public void testGeoJSONPolygonFeature() throws Exception {
+ StringBuilder b = new StringBuilder();
+ b.append("{ \"type\": \"Feature\",\n");
+ b.append(" \"geometry\": {\n");
+ b.append(" \"type\": \"Polygon\",\n");
+ b.append(" \"coordinates\": [\n");
+ b.append(" [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0],\n");
+ b.append(" [100.0, 1.0], [100.0, 0.0] ]\n");
+ b.append(" ]\n");
+ b.append(" },\n");
+ b.append(" \"properties\": {\n");
+ b.append(" \"prop0\": \"value0\",\n");
+ b.append(" \"prop1\": {\"this\": \"that\"}\n");
+ b.append(" }\n");
+ b.append("}\n");
+
+ Polygon[] polygons = Polygon.fromGeoJSON(b.toString());
+ assertEquals(1, polygons.length);
+ assertEquals(new Polygon(new double[] {0.0, 0.0, 1.0, 1.0, 0.0},
+ new double[] {100.0, 101.0, 101.0, 100.0, 100.0}), polygons[0]);
+ }
+
+ // make sure MultiPolygon inside a type: Feature also works
+ public void testGeoJSONMultiPolygonFeature() throws Exception {
+ StringBuilder b = new StringBuilder();
+ b.append("{ \"type\": \"Feature\",\n");
+ b.append(" \"geometry\": {\n");
+ b.append(" \"type\": \"MultiPolygon\",\n");
+ b.append(" \"coordinates\": [\n");
+ b.append(" [\n");
+ b.append(" [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0],\n");
+ b.append(" [100.0, 1.0], [100.0, 0.0] ]\n");
+ b.append(" ],\n");
+ b.append(" [\n");
+ b.append(" [ [10.0, 2.0], [11.0, 2.0], [11.0, 3.0],\n");
+ b.append(" [10.0, 3.0], [10.0, 2.0] ]\n");
+ b.append(" ]\n");
+ b.append(" ]\n");
+ b.append(" },\n");
+ b.append(" \"properties\": {\n");
+ b.append(" \"prop0\": \"value0\",\n");
+ b.append(" \"prop1\": {\"this\": \"that\"}\n");
+ b.append(" }\n");
+ b.append("}\n");
+
+ Polygon[] polygons = Polygon.fromGeoJSON(b.toString());
+ assertEquals(2, polygons.length);
+ assertEquals(new Polygon(new double[] {0.0, 0.0, 1.0, 1.0, 0.0},
+ new double[] {100.0, 101.0, 101.0, 100.0, 100.0}), polygons[0]);
+ assertEquals(new Polygon(new double[] {2.0, 2.0, 3.0, 3.0, 2.0},
+ new double[] {10.0, 11.0, 11.0, 10.0, 10.0}), polygons[1]);
+ }
+
+ // FeatureCollection with one geometry is allowed:
+ public void testGeoJSONFeatureCollectionWithSinglePolygon() throws Exception {
+ StringBuilder b = new StringBuilder();
+ b.append("{ \"type\": \"FeatureCollection\",\n");
+ b.append(" \"features\": [\n");
+ b.append(" { \"type\": \"Feature\",\n");
+ b.append(" \"geometry\": {\n");
+ b.append(" \"type\": \"Polygon\",\n");
+ b.append(" \"coordinates\": [\n");
+ b.append(" [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0],\n");
+ b.append(" [100.0, 1.0], [100.0, 0.0] ]\n");
+ b.append(" ]\n");
+ b.append(" },\n");
+ b.append(" \"properties\": {\n");
+ b.append(" \"prop0\": \"value0\",\n");
+ b.append(" \"prop1\": {\"this\": \"that\"}\n");
+ b.append(" }\n");
+ b.append(" }\n");
+ b.append(" ]\n");
+ b.append("} \n");
+
+ Polygon expected = new Polygon(new double[] {0.0, 0.0, 1.0, 1.0, 0.0},
+ new double[] {100.0, 101.0, 101.0, 100.0, 100.0});
+ Polygon[] actual = Polygon.fromGeoJSON(b.toString());
+ assertEquals(1, actual.length);
+ assertEquals(expected, actual[0]);
+ }
+
+ // stuff after the object is not allowed
+ public void testIllegalGeoJSONExtraCrapAtEnd() throws Exception {
+ StringBuilder b = new StringBuilder();
+ b.append("{\n");
+ b.append(" \"type\": \"Polygon\",\n");
+ b.append(" \"coordinates\": [\n");
+ b.append(" [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0],\n");
+ b.append(" [100.0, 1.0], [100.0, 0.0] ]\n");
+ b.append(" ]\n");
+ b.append("}\n");
+ b.append("foo\n");
+
+ Exception e = expectThrows(ParseException.class, () -> Polygon.fromGeoJSON(b.toString()));
+ assertTrue(e.getMessage().contains("unexpected character 'f' after end of GeoJSON object"));
+ }
+
+ public void testIllegalGeoJSONLinkedCRS() throws Exception {
+
+ StringBuilder b = new StringBuilder();
+ b.append("{\n");
+ b.append(" \"type\": \"Polygon\",\n");
+ b.append(" \"coordinates\": [\n");
+ b.append(" [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0],\n");
+ b.append(" [100.0, 1.0], [100.0, 0.0] ]\n");
+ b.append(" ],\n");
+ b.append(" \"crs\": {\n");
+ b.append(" \"type\": \"link\",\n");
+ b.append(" \"properties\": {\n");
+ b.append(" \"href\": \"http://example.com/crs/42\",\n");
+ b.append(" \"type\": \"proj4\"\n");
+ b.append(" }\n");
+ b.append(" } \n");
+ b.append("}\n");
+ Exception e = expectThrows(ParseException.class, () -> Polygon.fromGeoJSON(b.toString()));
+ assertTrue(e.getMessage().contains("cannot handle linked crs"));
+ }
+
+ // FeatureCollection with more than one geometry is not supported:
+ public void testIllegalGeoJSONMultipleFeatures() throws Exception {
+ StringBuilder b = new StringBuilder();
+ b.append("{ \"type\": \"FeatureCollection\",\n");
+ b.append(" \"features\": [\n");
+ b.append(" { \"type\": \"Feature\",\n");
+ b.append(" \"geometry\": {\"type\": \"Point\", \"coordinates\": [102.0, 0.5]},\n");
+ b.append(" \"properties\": {\"prop0\": \"value0\"}\n");
+ b.append(" },\n");
+ b.append(" { \"type\": \"Feature\",\n");
+ b.append(" \"geometry\": {\n");
+ b.append(" \"type\": \"LineString\",\n");
+ b.append(" \"coordinates\": [\n");
+ b.append(" [102.0, 0.0], [103.0, 1.0], [104.0, 0.0], [105.0, 1.0]\n");
+ b.append(" ]\n");
+ b.append(" },\n");
+ b.append(" \"properties\": {\n");
+ b.append(" \"prop0\": \"value0\",\n");
+ b.append(" \"prop1\": 0.0\n");
+ b.append(" }\n");
+ b.append(" },\n");
+ b.append(" { \"type\": \"Feature\",\n");
+ b.append(" \"geometry\": {\n");
+ b.append(" \"type\": \"Polygon\",\n");
+ b.append(" \"coordinates\": [\n");
+ b.append(" [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0],\n");
+ b.append(" [100.0, 1.0], [100.0, 0.0] ]\n");
+ b.append(" ]\n");
+ b.append(" },\n");
+ b.append(" \"properties\": {\n");
+ b.append(" \"prop0\": \"value0\",\n");
+ b.append(" \"prop1\": {\"this\": \"that\"}\n");
+ b.append(" }\n");
+ b.append(" }\n");
+ b.append(" ]\n");
+ b.append("} \n");
+
+ Exception e = expectThrows(ParseException.class, () -> Polygon.fromGeoJSON(b.toString()));
+ assertTrue(e.getMessage().contains("can only handle type FeatureCollection (if it has a single polygon geometry), Feature, Polygon or MutiPolygon, but got Point"));
+ }
}