diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt index 23d807b700f..4097ce07938 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 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 00000000000..17b595f7139 --- /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 = ""; + 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 lats = new ArrayList(); + ArrayList 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 lats = new ArrayList(); + ArrayList 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 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 lats = new ArrayList(); + ArrayList 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 lats = new ArrayList(); + ArrayList lons = new ArrayList(); + parseCoordinates(stream, lats, lons); + ArrayList 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 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 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 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 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 00000000000..e941ef411bb --- /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); + } +}