You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by sa...@apache.org on 2016/07/21 13:37:44 UTC

[22/51] [abbrv] lucene-solr:apiv2: LUCENE-7380: add Polygon.fromGeoJSON

LUCENE-7380: add Polygon.fromGeoJSON


Project: http://git-wip-us.apache.org/repos/asf/lucene-solr/repo
Commit: http://git-wip-us.apache.org/repos/asf/lucene-solr/commit/343f374b
Tree: http://git-wip-us.apache.org/repos/asf/lucene-solr/tree/343f374b
Diff: http://git-wip-us.apache.org/repos/asf/lucene-solr/diff/343f374b

Branch: refs/heads/apiv2
Commit: 343f374b530fa71dc6102d74725b536f5f1367f3
Parents: 2e0b2f5
Author: Mike McCandless <mi...@apache.org>
Authored: Fri Jul 15 10:51:08 2016 -0400
Committer: Mike McCandless <mi...@apache.org>
Committed: Fri Jul 15 10:51:08 2016 -0400

----------------------------------------------------------------------
 lucene/CHANGES.txt                              |   4 +
 .../src/java/org/apache/lucene/geo/Polygon.java |  13 +-
 .../lucene/geo/SimpleGeoJSONPolygonParser.java  | 440 +++++++++++++++++++
 .../test/org/apache/lucene/geo/TestPolygon.java | 241 ++++++++++
 4 files changed, 697 insertions(+), 1 deletion(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/343f374b/lucene/CHANGES.txt
----------------------------------------------------------------------
diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt
index 9e19d35..92ee7b9 100644
--- a/lucene/CHANGES.txt
+++ b/lucene/CHANGES.txt
@@ -54,6 +54,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)

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/343f374b/lucene/core/src/java/org/apache/lucene/geo/Polygon.java
----------------------------------------------------------------------
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 3b5dec9..99453b9 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
+ * <a href="http://geojson.org/geojson-spec.html">GeoJSON</a> string.
  * <p>
  * NOTES:
  * <ol>
@@ -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.
+   *
+   *  <p>See <a href="http://geojson.org/geojson-spec.html">the GeoJSON specification</a>. */
+  public static Polygon[] fromGeoJSON(String geojson) throws ParseException {
+    return new SimpleGeoJSONPolygonParser(geojson).parse();
+  }
 }

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/343f374b/lucene/core/src/java/org/apache/lucene/geo/SimpleGeoJSONPolygonParser.java
----------------------------------------------------------------------
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 0000000..278307f
--- /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<Object> 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<Polygon> polygons = new ArrayList<>();
+      for(int i=0;i<coordinates.size();i++) {
+        Object o = coordinates.get(i);
+        if (o instanceof List == false) {
+          throw newParseException("elements of coordinates array should be an array, but got: " + o.getClass());
+        }
+        polygons.add(parsePolygon((List<Object>) 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<Object>) 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<Object> coordinates) throws ParseException {
+    List<Polygon> 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<Object>) o);
+    for(int i=1;i<coordinates.size();i++) {
+      o = coordinates.get(i);
+      if (o instanceof List == false) {
+        throw newParseException("elements of coordinates array must be an array [[lat, lon], [lat, lon] ...] but got: " + o);
+      }
+      double[][] holePoints = parsePoints((List<Object>) 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<Object> o) throws ParseException {
+    double[] lats = new double[o.size()];
+    double[] lons = new double[o.size()];
+    for(int i=0;i<o.size();i++) {
+      Object point = o.get(i);
+      if (point instanceof List == false) {
+        throw newParseException("elements of coordinates array must [lat, lon] array, but got: " + point);
+      }
+      List<Object> pointList = (List<Object>) 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<Object> parseArray(String path) throws ParseException {
+    List<Object> 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);
+  }
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/343f374b/lucene/core/src/test/org/apache/lucene/geo/TestPolygon.java
----------------------------------------------------------------------
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 401092f..8ee6271 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"));
+  }
 }