lucene-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From a.@apache.org
Subject [03/32] lucene-solr:jira/solr-12730: LUCENE-8538: Add a Simple WKT Shape Parser for creating Lucene Geometries (Polygon, Line, Rectangle, points, and multi-variants) from WKT format.
Date Mon, 29 Oct 2018 19:07:27 GMT
LUCENE-8538: Add a Simple WKT Shape Parser for creating Lucene Geometries (Polygon, Line, Rectangle,
points, and multi-variants) from WKT format.


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

Branch: refs/heads/jira/solr-12730
Commit: bd714ca1edc681cd871e6d385bd98a9cb9d7ffe6
Parents: e083b15
Author: Nicholas Knize <nknize@gmail.com>
Authored: Mon Oct 22 14:25:22 2018 -0500
Committer: Nicholas Knize <nknize@gmail.com>
Committed: Wed Oct 24 09:27:47 2018 -0500

----------------------------------------------------------------------
 lucene/CHANGES.txt                              |   3 +
 .../apache/lucene/geo/SimpleWKTShapeParser.java | 406 +++++++++++++++++++
 .../lucene/geo/TestSimpleWKTShapeParsing.java   | 206 ++++++++++
 3 files changed, 615 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/bd714ca1/lucene/CHANGES.txt
----------------------------------------------------------------------
diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt
index 23d807b..4097ce0 100644
--- a/lucene/CHANGES.txt
+++ b/lucene/CHANGES.txt
@@ -220,6 +220,9 @@ New Features
   may be used to determine how to split the inner nodes, and dimensions N+1 to D
   are ignored and stored as data dimensions at the leaves. (Nick Knize)
 
+* LUCENE-8538: Add a Simple WKT Shape Parser for creating Lucene Geometries (Polygon, Line,
+  Rectangle) from WKT format. (Nick Knize)
+
 Improvements:
 
 * LUCENE-8521: Change LatLonShape encoding to 7 dimensions instead of 6; where the

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/bd714ca1/lucene/sandbox/src/java/org/apache/lucene/geo/SimpleWKTShapeParser.java
----------------------------------------------------------------------
diff --git a/lucene/sandbox/src/java/org/apache/lucene/geo/SimpleWKTShapeParser.java b/lucene/sandbox/src/java/org/apache/lucene/geo/SimpleWKTShapeParser.java
new file mode 100644
index 0000000..17b595f
--- /dev/null
+++ b/lucene/sandbox/src/java/org/apache/lucene/geo/SimpleWKTShapeParser.java
@@ -0,0 +1,406 @@
+/*
+ * 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.io.IOException;
+import java.io.StreamTokenizer;
+import java.io.StringReader;
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * Parses shape geometry represented in WKT format
+ *
+ * complies with OGC® document: 12-063r5 and ISO/IEC 13249-3:2016 standard
+ * located at http://docs.opengeospatial.org/is/12-063r5/12-063r5.html
+ */
+public class SimpleWKTShapeParser {
+  public static final String EMPTY = "EMPTY";
+  public static final String SPACE = " ";
+  public static final String LPAREN = "(";
+  public static final String RPAREN = ")";
+  public static final String COMMA = ",";
+  public static final String NAN = "NaN";
+
+  private static final String NUMBER = "<NUMBER>";
+  private static final String EOF = "END-OF-STREAM";
+  private static final String EOL = "END-OF-LINE";
+
+  // no instance
+  private SimpleWKTShapeParser() {}
+
+  public static Object parse(String wkt) throws IOException, ParseException {
+    return parseExpectedType(wkt, null);
+  }
+
+  public static Object parseExpectedType(String wkt, final ShapeType shapeType) throws IOException,
ParseException {
+    try (StringReader reader = new StringReader(wkt)) {
+      // setup the tokenizer; configured to read words w/o numbers
+      StreamTokenizer tokenizer = new StreamTokenizer(reader);
+      tokenizer.resetSyntax();
+      tokenizer.wordChars('a', 'z');
+      tokenizer.wordChars('A', 'Z');
+      tokenizer.wordChars(128 + 32, 255);
+      tokenizer.wordChars('0', '9');
+      tokenizer.wordChars('-', '-');
+      tokenizer.wordChars('+', '+');
+      tokenizer.wordChars('.', '.');
+      tokenizer.whitespaceChars(0, ' ');
+      tokenizer.commentChar('#');
+      Object geometry = parseGeometry(tokenizer, shapeType);
+      checkEOF(tokenizer);
+      return geometry;
+    }
+  }
+
+  /** parse geometry from the stream tokenizer */
+  private static Object parseGeometry(StreamTokenizer stream, ShapeType shapeType) throws
IOException, ParseException {
+    final ShapeType type = ShapeType.forName(nextWord(stream));
+    if (shapeType != null && shapeType != ShapeType.GEOMETRYCOLLECTION) {
+      if (type.wktName().equals(shapeType.wktName()) == false) {
+        throw new ParseException("Expected geometry type: [" + shapeType + "], but found:
[" + type + "]", stream.lineno());
+      }
+    }
+    switch (type) {
+      case POINT:
+        return parsePoint(stream);
+      case MULTIPOINT:
+        return parseMultiPoint(stream);
+      case LINESTRING:
+        return parseLine(stream);
+      case MULTILINESTRING:
+        return parseMultiLine(stream);
+      case POLYGON:
+        return parsePolygon(stream);
+      case MULTIPOLYGON:
+        return parseMultiPolygon(stream);
+      case ENVELOPE:
+        return parseBBox(stream);
+      case GEOMETRYCOLLECTION:
+        return parseGeometryCollection(stream);
+      default:
+        throw new IllegalArgumentException("Unknown geometry type: " + type);
+    }
+  }
+
+  /** Parses a point as a double array */
+  private static double[] parsePoint(StreamTokenizer stream) throws IOException, ParseException
{
+    if (nextEmptyOrOpen(stream).equals(EMPTY)) {
+      return null;
+    }
+    double[] pt = new double[]{nextNumber(stream), nextNumber(stream)};
+    if (isNumberNext(stream) == true) {
+      nextNumber(stream);
+    }
+    nextCloser(stream);
+    return pt;
+  }
+
+  /** Parses a list of points into latitude and longitude arraylists */
+  private static void parseCoordinates(StreamTokenizer stream, ArrayList lats, ArrayList
lons)
+      throws IOException, ParseException {
+    boolean isOpenParen = false;
+    if (isNumberNext(stream) || (isOpenParen = nextWord(stream).equals(LPAREN))) {
+      parseCoordinate(stream, lats, lons);
+    }
+
+    while (nextCloserOrComma(stream).equals(COMMA)) {
+      isOpenParen = false;
+      if (isNumberNext(stream) || (isOpenParen = nextWord(stream).equals(LPAREN))) {
+        parseCoordinate(stream, lats, lons);
+      }
+      if (isOpenParen && nextCloser(stream).equals(RPAREN) == false) {
+        throw new ParseException("expected: [" + RPAREN + "] but found: [" + tokenString(stream)
+ "]", stream.lineno());
+      }
+    }
+
+    if (isOpenParen && nextCloser(stream).equals(RPAREN) == false) {
+      throw new ParseException("expected: [" + RPAREN + "] but found: [" + tokenString(stream)
+ "]", stream.lineno());
+    }
+  }
+
+  /** parses a single coordinate, w/ optional 3rd dimension */
+  private static void parseCoordinate(StreamTokenizer stream, ArrayList lats, ArrayList lons)
+      throws IOException, ParseException {
+    lons.add(nextNumber(stream));
+    lats.add(nextNumber(stream));
+    if (isNumberNext(stream)) {
+      nextNumber(stream);
+    }
+  }
+
+  /** parses a MULTIPOINT type */
+  private static double[][] parseMultiPoint(StreamTokenizer stream) throws IOException, ParseException
{
+    String token = nextEmptyOrOpen(stream);
+    if (token.equals(EMPTY)) {
+      return null;
+    }
+    ArrayList<Double> lats = new ArrayList();
+    ArrayList<Double> lons = new ArrayList();
+    parseCoordinates(stream, lats, lons);
+    double[][] result = new double[lats.size()][2];
+    for (int i = 0; i < lats.size(); ++i) {
+      result[i] = new double[] {lons.get(i), lats.get(i)};
+    }
+    return result;
+  }
+
+  /** parses a LINESTRING */
+  private static Line parseLine(StreamTokenizer stream) throws IOException, ParseException
{
+    String token = nextEmptyOrOpen(stream);
+    if (token.equals(EMPTY)) {
+      return null;
+    }
+    ArrayList<Double> lats = new ArrayList();
+    ArrayList<Double> lons = new ArrayList();
+    parseCoordinates(stream, lats, lons);
+    return new Line(lats.stream().mapToDouble(i->i).toArray(), lons.stream().mapToDouble(i->i).toArray());
+  }
+
+  /** parses a MULTILINESTRING */
+  private static Line[] parseMultiLine(StreamTokenizer stream) throws IOException, ParseException
{
+    String token = nextEmptyOrOpen(stream);
+    if (token.equals(EMPTY)) {
+      return null;
+    }
+    ArrayList<Line> lines = new ArrayList();
+    lines.add(parseLine(stream));
+    while (nextCloserOrComma(stream).equals(COMMA)) {
+      lines.add(parseLine(stream));
+    }
+    return lines.toArray(new Line[lines.size()]);
+  }
+
+  /** parses the hole of a polygon */
+  private static Polygon parsePolygonHole(StreamTokenizer stream) throws IOException, ParseException
{
+    ArrayList<Double> lats = new ArrayList();
+    ArrayList<Double> lons = new ArrayList();
+    parseCoordinates(stream, lats, lons);
+    return new Polygon(lats.stream().mapToDouble(i->i).toArray(), lons.stream().mapToDouble(i->i).toArray());
+  }
+
+  /** parses a POLYGON */
+  private static Polygon parsePolygon(StreamTokenizer stream) throws IOException, ParseException
{
+    if (nextEmptyOrOpen(stream).equals(EMPTY)) {
+      return null;
+    }
+    nextOpener(stream);
+    ArrayList<Double> lats = new ArrayList();
+    ArrayList<Double> lons = new ArrayList();
+    parseCoordinates(stream, lats, lons);
+    ArrayList<Polygon> holes = new ArrayList<>();
+    while (nextCloserOrComma(stream).equals(COMMA)) {
+      holes.add(parsePolygonHole(stream));
+    }
+
+    if (holes.isEmpty() == false) {
+      return new Polygon(lats.stream().mapToDouble(i->i).toArray(), lons.stream().mapToDouble(i->i).toArray(),
holes.toArray(new Polygon[holes.size()]));
+    }
+    return new Polygon(lats.stream().mapToDouble(i->i).toArray(), lons.stream().mapToDouble(i->i).toArray());
+  }
+
+  /** parses a MULTIPOLYGON */
+  private static Polygon[] parseMultiPolygon(StreamTokenizer stream) throws IOException,
ParseException {
+    String token = nextEmptyOrOpen(stream);
+    if (token.equals(EMPTY)) {
+      return null;
+    }
+    ArrayList<Polygon> polygons = new ArrayList();
+    polygons.add(parsePolygon(stream));
+    while (nextCloserOrComma(stream).equals(COMMA)) {
+      polygons.add(parsePolygon(stream));
+    }
+    return polygons.toArray(new Polygon[polygons.size()]);
+  }
+
+  /** parses an ENVELOPE */
+  private static Rectangle parseBBox(StreamTokenizer stream) throws IOException, ParseException
{
+    if (nextEmptyOrOpen(stream).equals(EMPTY)) {
+      return null;
+    }
+    double minLon = nextNumber(stream);
+    nextComma(stream);
+    double maxLon = nextNumber(stream);
+    nextComma(stream);
+    double maxLat = nextNumber(stream);
+    nextComma(stream);
+    double minLat = nextNumber(stream);
+    nextCloser(stream);
+    return new Rectangle(minLat, maxLat, minLon, maxLon);
+  }
+
+  /** parses a GEOMETRYCOLLECTION */
+  private static Object[] parseGeometryCollection(StreamTokenizer stream) throws IOException,
ParseException {
+    if (nextEmptyOrOpen(stream).equals(EMPTY)) {
+      return null;
+    }
+    ArrayList<Object> geometries = new ArrayList<>();
+    geometries.add(parseGeometry(stream, ShapeType.GEOMETRYCOLLECTION));
+    while (nextCloserOrComma(stream).equals(COMMA)) {
+      geometries.add(parseGeometry(stream, null));
+    }
+    return geometries.toArray(new Object[geometries.size()]);
+  }
+
+  /** next word in the stream */
+  private static String nextWord(StreamTokenizer stream) throws ParseException, IOException
{
+    switch (stream.nextToken()) {
+      case StreamTokenizer.TT_WORD:
+        final String word = stream.sval;
+        return word.equalsIgnoreCase(EMPTY) ? EMPTY : word;
+      case '(': return LPAREN;
+      case ')': return RPAREN;
+      case ',': return COMMA;
+    }
+    throw new ParseException("expected word but found: " + tokenString(stream), stream.lineno());
+  }
+
+  /** next number in the stream */
+  private static double nextNumber(StreamTokenizer stream) throws IOException, ParseException
{
+    if (stream.nextToken() == StreamTokenizer.TT_WORD) {
+      if (stream.sval.equalsIgnoreCase(NAN)) {
+        return Double.NaN;
+      } else {
+        try {
+          return Double.parseDouble(stream.sval);
+        } catch (NumberFormatException e) {
+          throw new ParseException("invalid number found: " + stream.sval, stream.lineno());
+        }
+      }
+    }
+    throw new ParseException("expected number but found: " + tokenString(stream), stream.lineno());
+  }
+
+  /** next token in the stream */
+  private static String tokenString(StreamTokenizer stream) {
+    switch (stream.ttype) {
+      case StreamTokenizer.TT_WORD: return stream.sval;
+      case StreamTokenizer.TT_EOF: return EOF;
+      case StreamTokenizer.TT_EOL: return EOL;
+      case StreamTokenizer.TT_NUMBER: return NUMBER;
+    }
+    return "'" + (char)stream.ttype + "'";
+  }
+
+  /** checks if the next token is a number */
+  private static boolean isNumberNext(StreamTokenizer stream) throws IOException {
+    final int type = stream.nextToken();
+    stream.pushBack();
+    return type == StreamTokenizer.TT_WORD;
+  }
+
+  /** checks if next token is an EMPTY or open paren */
+  private static String nextEmptyOrOpen(StreamTokenizer stream) throws IOException, ParseException
{
+    final String next = nextWord(stream);
+    if (next.equals(EMPTY) || next.equals(LPAREN)) {
+      return next;
+    }
+    throw new ParseException("expected " + EMPTY + " or " + LPAREN
+        + " but found: " + tokenString(stream), stream.lineno());
+  }
+
+  /** checks if next token is a closing paren */
+  private static String nextCloser(StreamTokenizer stream) throws IOException, ParseException
{
+    if (nextWord(stream).equals(RPAREN)) {
+      return RPAREN;
+    }
+    throw new ParseException("expected " + RPAREN + " but found: " + tokenString(stream),
stream.lineno());
+  }
+
+  /** expects a comma as next token */
+  private static String nextComma(StreamTokenizer stream) throws IOException, ParseException
{
+    if (nextWord(stream).equals(COMMA) == true) {
+      return COMMA;
+    }
+    throw new ParseException("expected " + COMMA + " but found: " + tokenString(stream),
stream.lineno());
+  }
+
+  /** expects an open RPAREN as the next toke */
+  private static String nextOpener(StreamTokenizer stream) throws IOException, ParseException
{
+    if (nextWord(stream).equals(LPAREN)) {
+      return LPAREN;
+    }
+    throw new ParseException("expected " + LPAREN + " but found: " + tokenString(stream),
stream.lineno());
+  }
+
+  /** expects either a closing LPAREN or comma as the next token */
+  private static String nextCloserOrComma(StreamTokenizer stream) throws IOException, ParseException
{
+    String token = nextWord(stream);
+    if (token.equals(COMMA) || token.equals(RPAREN)) {
+      return token;
+    }
+    throw new ParseException("expected " + COMMA + " or " + RPAREN
+        + " but found: " + tokenString(stream), stream.lineno());
+  }
+
+  /** next word in the stream */
+  private static void checkEOF(StreamTokenizer stream) throws ParseException, IOException
{
+    if (stream.nextToken() != StreamTokenizer.TT_EOF) {
+      throw new ParseException("expected end of WKT string but found additional text: "
+          + tokenString(stream), stream.lineno());
+    }
+  }
+
+  /** Enumerated type for Shapes */
+  public enum ShapeType {
+    POINT("point"),
+    MULTIPOINT("multipoint"),
+    LINESTRING("linestring"),
+    MULTILINESTRING("multilinestring"),
+    POLYGON("polygon"),
+    MULTIPOLYGON("multipolygon"),
+    GEOMETRYCOLLECTION("geometrycollection"),
+    ENVELOPE("envelope"); // not part of the actual WKB spec
+
+    private final String shapeName;
+    private static Map<String, ShapeType> shapeTypeMap = new HashMap<>();
+    private static final String BBOX = "BBOX";
+
+    static {
+      for (ShapeType type : values()) {
+        shapeTypeMap.put(type.shapeName, type);
+      }
+      shapeTypeMap.put(ENVELOPE.wktName().toLowerCase(Locale.ROOT), ENVELOPE);
+    }
+
+    ShapeType(String shapeName) {
+      this.shapeName = shapeName;
+    }
+
+    protected String typename() {
+      return shapeName;
+    }
+
+    /** wkt shape name */
+    public String wktName() {
+      return this == ENVELOPE ? BBOX : this.shapeName;
+    }
+
+    public static ShapeType forName(String shapename) {
+      String typename = shapename.toLowerCase(Locale.ROOT);
+      for (ShapeType type : values()) {
+        if(type.shapeName.equals(typename)) {
+          return type;
+        }
+      }
+      throw new IllegalArgumentException("unknown geo_shape ["+shapename+"]");
+    }
+  }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/bd714ca1/lucene/sandbox/src/test/org/apache/lucene/geo/TestSimpleWKTShapeParsing.java
----------------------------------------------------------------------
diff --git a/lucene/sandbox/src/test/org/apache/lucene/geo/TestSimpleWKTShapeParsing.java
b/lucene/sandbox/src/test/org/apache/lucene/geo/TestSimpleWKTShapeParsing.java
new file mode 100644
index 0000000..e941ef4
--- /dev/null
+++ b/lucene/sandbox/src/test/org/apache/lucene/geo/TestSimpleWKTShapeParsing.java
@@ -0,0 +1,206 @@
+/*
+ * 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 org.apache.lucene.geo.SimpleWKTShapeParser.ShapeType;
+import org.apache.lucene.util.LuceneTestCase;
+
+/** simple WKT parsing tests */
+public class TestSimpleWKTShapeParsing extends LuceneTestCase {
+
+  /** test simple Point */
+  public void testPoint() throws Exception {
+    StringBuilder b = new StringBuilder();
+    b.append(ShapeType.POINT + "(101.0 10.0)");
+    Object shape = SimpleWKTShapeParser.parse(b.toString());
+
+    assertTrue(shape instanceof double[]);
+    double[] point = (double[])shape;
+    assertEquals(101d, point[0], 0d);  // lon
+    assertEquals(10d, point[1], 1d);   // lat
+  }
+
+  /** test POINT EMPTY returns null */
+  public void testEmptyPoint() throws Exception {
+    StringBuilder b = new StringBuilder();
+    b.append(ShapeType.POINT + SimpleWKTShapeParser.SPACE + SimpleWKTShapeParser.EMPTY);
+    Object shape = SimpleWKTShapeParser.parse(b.toString());
+    assertNull(shape);
+  }
+
+  /** test simple MULTIPOINT */
+  public void testMultiPoint() throws Exception {
+    StringBuilder b = new StringBuilder();
+    b.append(ShapeType.MULTIPOINT + "(101.0 10.0, 180.0 90.0, -180.0 -90.0)");
+    Object shape = SimpleWKTShapeParser.parse(b.toString());
+
+    assertTrue(shape instanceof double[][]);
+    double[][] pts = (double[][])shape;
+    assertEquals(3, pts.length,0);
+    assertEquals(101d, pts[0][0], 0);
+    assertEquals(10d, pts[0][1], 0);
+    assertEquals(180d, pts[1][0], 0);
+    assertEquals(90d, pts[1][1], 0);
+    assertEquals(-180d, pts[2][0], 0);
+    assertEquals(-90d, pts[2][1], 0);
+  }
+
+  /** test MULTIPOINT EMPTY returns null */
+  public void testEmptyMultiPoint() throws Exception {
+    StringBuilder b = new StringBuilder();
+    b.append(ShapeType.MULTIPOINT + SimpleWKTShapeParser.SPACE + SimpleWKTShapeParser.EMPTY);
+    Object shape = SimpleWKTShapeParser.parse(b.toString());
+    assertNull(shape);
+  }
+
+  /** test simple LINESTRING */
+  public void testLine() throws Exception {
+    StringBuilder b = new StringBuilder();
+    b.append(ShapeType.LINESTRING + "(101.0 10.0, 180.0 90.0, -180.0 -90.0)");
+    Object shape = SimpleWKTShapeParser.parse(b.toString());
+
+    assertTrue(shape instanceof Line);
+    Line line = (Line)shape;
+    assertEquals(3, line.numPoints(),0);
+    assertEquals(101d, line.getLon(0), 0);
+    assertEquals(10d, line.getLat(0), 0);
+    assertEquals(180d, line.getLon(1), 0);
+    assertEquals(90d, line.getLat(1), 0);
+    assertEquals(-180d, line.getLon(2), 0);
+    assertEquals(-90d, line.getLat(2), 0);
+  }
+
+  /** test empty LINESTRING */
+  public void testEmptyLine() throws Exception {
+    StringBuilder b = new StringBuilder();
+    b.append(ShapeType.LINESTRING + SimpleWKTShapeParser.SPACE + SimpleWKTShapeParser.EMPTY);
+    Object shape = SimpleWKTShapeParser.parse(b.toString());
+    assertNull(shape);
+  }
+
+  /** test simple MULTILINESTRING */
+  public void testMultiLine() throws Exception {
+    StringBuilder b = new StringBuilder();
+    b.append(ShapeType.MULTILINESTRING + "((100.0 0.0, 101.0 0.0, 101.0 1.0, 100.0 1.0, 100.0
0.0),");
+    b.append("(10.0 2.0, 11.0 2.0, 11.0 3.0, 10.0 3.0, 10.0 2.0))");
+    Object shape = SimpleWKTShapeParser.parse(b.toString());
+
+    assertTrue(shape instanceof Line[]);
+    Line[] lines = (Line[])shape;
+    assertEquals(2, lines.length, 0);
+  }
+
+  /** test empty MULTILINESTRING */
+  public void testEmptyMultiLine() throws Exception {
+    StringBuilder b = new StringBuilder();
+    b.append(ShapeType.MULTILINESTRING + SimpleWKTShapeParser.SPACE + SimpleWKTShapeParser.EMPTY);
+    Object shape = SimpleWKTShapeParser.parse(b.toString());
+    assertNull(shape);
+  }
+
+  /** test simple polygon: POLYGON((100.0 0.0, 101.0 0.0, 101.0 1.0, 100.0 1.0, 100.0 0.0))
*/
+  public void testPolygon() throws Exception {
+    StringBuilder b = new StringBuilder();
+    b.append(ShapeType.POLYGON + "((100.0 0.0, 101.0 0.0, 101.0 1.0, 100.0 1.0, 100.0 0.0))\n");
+    Object shape = SimpleWKTShapeParser.parse(b.toString());
+
+    assertTrue(shape instanceof Polygon);
+    Polygon polygon = (Polygon)shape;
+    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}), polygon);
+  }
+
+  /** test polygon with hole */
+  public void testPolygonWithHole() throws Exception {
+    StringBuilder b = new StringBuilder();
+    b.append(ShapeType.POLYGON + "((100.0 0.0, 101.0 0.0, 101.0 1.0, 100.0 1.0, 100.0 0.0),
");
+    b.append("(100.5 0.5, 100.5 0.75, 100.75 0.75, 100.75 0.5, 100.5 0.5))");
+    Object shape = SimpleWKTShapeParser.parse(b.toString());
+
+    assertTrue(shape instanceof Polygon);
+    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 polygon = (Polygon)shape;
+
+    assertEquals(expected, polygon);
+  }
+
+  /** test MultiPolygon returns Polygon array */
+  public void testMultiPolygon() throws Exception {
+    StringBuilder b = new StringBuilder();
+    b.append(ShapeType.MULTIPOLYGON + "(((100.0 0.0, 101.0 0.0, 101.0 1.0, 100.0 1.0, 100.0
0.0)),");
+    b.append("((10.0 2.0, 11.0 2.0, 11.0 3.0, 10.0 3.0, 10.0 2.0)))");
+    Object shape = SimpleWKTShapeParser.parse(b.toString());
+
+    assertTrue(shape instanceof Polygon[]);
+    Polygon[] polygons = (Polygon[])shape;
+    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]);
+  }
+
+  /** polygon must be closed */
+  public void testPolygonNotClosed() {
+    StringBuilder b = new StringBuilder();
+    b.append(ShapeType.POLYGON + "((100.0 0.0, 101.0 0.0, 101.0 1.0, 100.0 1.0))\n");
+
+    IllegalArgumentException expected = expectThrows(IllegalArgumentException.class, () ->
{
+      SimpleWKTShapeParser.parse(b.toString());
+    });
+    assertTrue(expected.getMessage(),
+        expected.getMessage().contains("first and last points of the polygon must be the
same (it must close itself)"));
+  }
+
+  /** test simple ENVELOPE (minLon, maxLon, maxLat, minLat) */
+  public void testEnvelope() throws Exception {
+    StringBuilder b = new StringBuilder();
+    b.append(ShapeType.ENVELOPE + "(-180.0, 180.0, 90.0, -90.0)");
+    Object shape = SimpleWKTShapeParser.parse(b.toString());
+
+    assertTrue(shape instanceof Rectangle);
+    Rectangle bbox = (Rectangle)shape;
+    assertEquals(-180d, bbox.minLon, 0);
+    assertEquals(180d, bbox.maxLon, 0);
+    assertEquals(-90d, bbox.minLat, 0);
+    assertEquals(90d, bbox.maxLat, 0);
+  }
+
+  /** test simple geometry collection */
+  public void testGeometryCollection() throws Exception {
+    StringBuilder b = new StringBuilder();
+    b.append(ShapeType.GEOMETRYCOLLECTION + "(");
+    b.append(ShapeType.MULTIPOLYGON + "(((100.0 0.0, 101.0 0.0, 101.0 1.0, 100.0 1.0, 100.0
0.0)),");
+    b.append("((10.0 2.0, 11.0 2.0, 11.0 3.0, 10.0 3.0, 10.0 2.0))),");
+    b.append(ShapeType.POINT + "(101.0 10.0),");
+    b.append(ShapeType.LINESTRING + "(101.0 10.0, 180.0 90.0, -180.0 -90.0),");
+    b.append(ShapeType.ENVELOPE + "(-180.0, 180.0, 90.0, -90.0)");
+    b.append(")");
+    Object shape = SimpleWKTShapeParser.parse(b.toString());
+
+    assertTrue(shape instanceof Object[]);
+    Object[] shapes = (Object[]) shape;
+    assertEquals(4, shapes.length);
+    assertTrue(shapes[0] instanceof Polygon[]);
+    assertTrue(shapes[1] instanceof double[]);
+    assertTrue(shapes[2] instanceof Line);
+    assertTrue(shapes[3] instanceof Rectangle);
+  }
+}


Mime
View raw message