mirror of https://github.com/apache/lucene.git
LUCENE-8538: Add a Simple WKT Shape Parser for creating Lucene Geometries (Polygon, Line, Rectangle, points, and multi-variants) from WKT format.
This commit is contained in:
parent
e083b1501e
commit
bd714ca1ed
|
@ -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
|
||||
|
|
|
@ -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+"]");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue