diff --git a/core/src/main/java/org/elasticsearch/common/geo/GeoShapeType.java b/core/src/main/java/org/elasticsearch/common/geo/GeoShapeType.java index f8030296940..9eb1fa9a3f4 100644 --- a/core/src/main/java/org/elasticsearch/common/geo/GeoShapeType.java +++ b/core/src/main/java/org/elasticsearch/common/geo/GeoShapeType.java @@ -241,6 +241,11 @@ public enum GeoShapeType { } return coordinates; } + + @Override + public String wktName() { + return BBOX; + } }, CIRCLE("circle") { @Override @@ -273,11 +278,13 @@ public enum GeoShapeType { private final String shapename; private static Map shapeTypeMap = new HashMap<>(); + private static final String BBOX = "BBOX"; static { for (GeoShapeType type : values()) { shapeTypeMap.put(type.shapename, type); } + shapeTypeMap.put(ENVELOPE.wktName().toLowerCase(Locale.ROOT), ENVELOPE); } GeoShapeType(String shapename) { @@ -300,6 +307,11 @@ public enum GeoShapeType { ShapeBuilder.Orientation orientation, boolean coerce); abstract CoordinateNode validate(CoordinateNode coordinates, boolean coerce); + /** wkt shape name */ + public String wktName() { + return this.shapename; + } + public static List getShapeWriteables() { List namedWriteables = new ArrayList<>(); namedWriteables.add(new Entry(ShapeBuilder.class, PointBuilder.TYPE.shapeName(), PointBuilder::new)); @@ -313,4 +325,9 @@ public enum GeoShapeType { namedWriteables.add(new Entry(ShapeBuilder.class, GeometryCollectionBuilder.TYPE.shapeName(), GeometryCollectionBuilder::new)); return namedWriteables; } + + @Override + public String toString() { + return this.shapename; + } } diff --git a/core/src/main/java/org/elasticsearch/common/geo/builders/CircleBuilder.java b/core/src/main/java/org/elasticsearch/common/geo/builders/CircleBuilder.java index 108e66d9150..ecc33b94ae4 100644 --- a/core/src/main/java/org/elasticsearch/common/geo/builders/CircleBuilder.java +++ b/core/src/main/java/org/elasticsearch/common/geo/builders/CircleBuilder.java @@ -168,6 +168,11 @@ public class CircleBuilder extends ShapeBuilder { return TYPE; } + @Override + public String toWKT() { + throw new UnsupportedOperationException("The WKT spec does not support CIRCLE geometry"); + } + @Override public int hashCode() { return Objects.hash(center, radius, unit.ordinal()); diff --git a/core/src/main/java/org/elasticsearch/common/geo/builders/EnvelopeBuilder.java b/core/src/main/java/org/elasticsearch/common/geo/builders/EnvelopeBuilder.java index b352aa1d924..4949c363347 100644 --- a/core/src/main/java/org/elasticsearch/common/geo/builders/EnvelopeBuilder.java +++ b/core/src/main/java/org/elasticsearch/common/geo/builders/EnvelopeBuilder.java @@ -20,6 +20,7 @@ package org.elasticsearch.common.geo.builders; import org.elasticsearch.common.geo.GeoShapeType; +import org.elasticsearch.common.geo.parsers.GeoWKTParser; import org.elasticsearch.common.geo.parsers.ShapeParser; import org.locationtech.spatial4j.shape.Rectangle; import com.vividsolutions.jts.geom.Coordinate; @@ -70,6 +71,28 @@ public class EnvelopeBuilder extends ShapeBuilder { return this.bottomRight; } + @Override + protected StringBuilder contentToWKT() { + StringBuilder sb = new StringBuilder(); + + sb.append(GeoWKTParser.LPAREN); + // minX, maxX, maxY, minY + sb.append(topLeft.x); + sb.append(GeoWKTParser.COMMA); + sb.append(GeoWKTParser.SPACE); + sb.append(bottomRight.x); + sb.append(GeoWKTParser.COMMA); + sb.append(GeoWKTParser.SPACE); + // TODO support Z?? + sb.append(topLeft.y); + sb.append(GeoWKTParser.COMMA); + sb.append(GeoWKTParser.SPACE); + sb.append(bottomRight.y); + sb.append(GeoWKTParser.RPAREN); + + return sb; + } + @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); diff --git a/core/src/main/java/org/elasticsearch/common/geo/builders/GeometryCollectionBuilder.java b/core/src/main/java/org/elasticsearch/common/geo/builders/GeometryCollectionBuilder.java index 3ea422265a7..84052939da4 100644 --- a/core/src/main/java/org/elasticsearch/common/geo/builders/GeometryCollectionBuilder.java +++ b/core/src/main/java/org/elasticsearch/common/geo/builders/GeometryCollectionBuilder.java @@ -21,6 +21,7 @@ package org.elasticsearch.common.geo.builders; import org.elasticsearch.common.geo.GeoShapeType; import org.elasticsearch.common.geo.parsers.ShapeParser; +import org.elasticsearch.common.geo.parsers.GeoWKTParser; import org.locationtech.spatial4j.shape.Shape; import org.elasticsearch.ElasticsearchException; @@ -136,6 +137,23 @@ public class GeometryCollectionBuilder extends ShapeBuilder { return builder.endObject(); } + @Override + protected StringBuilder contentToWKT() { + StringBuilder sb = new StringBuilder(); + if (shapes.isEmpty()) { + sb.append(GeoWKTParser.EMPTY); + } else { + sb.append(GeoWKTParser.LPAREN); + sb.append(shapes.get(0).toWKT()); + for (int i = 1; i < shapes.size(); ++i) { + sb.append(GeoWKTParser.COMMA); + sb.append(shapes.get(i).toWKT()); + } + sb.append(GeoWKTParser.RPAREN); + } + return sb; + } + @Override public GeoShapeType type() { return TYPE; diff --git a/core/src/main/java/org/elasticsearch/common/geo/builders/MultiLineStringBuilder.java b/core/src/main/java/org/elasticsearch/common/geo/builders/MultiLineStringBuilder.java index 1a4f71da2d4..34a8960f69c 100644 --- a/core/src/main/java/org/elasticsearch/common/geo/builders/MultiLineStringBuilder.java +++ b/core/src/main/java/org/elasticsearch/common/geo/builders/MultiLineStringBuilder.java @@ -20,8 +20,8 @@ package org.elasticsearch.common.geo.builders; import org.elasticsearch.common.geo.GeoShapeType; +import org.elasticsearch.common.geo.parsers.GeoWKTParser; import org.elasticsearch.common.geo.parsers.ShapeParser; -import org.locationtech.spatial4j.shape.Shape; import com.vividsolutions.jts.geom.Coordinate; import com.vividsolutions.jts.geom.Geometry; import com.vividsolutions.jts.geom.LineString; @@ -82,6 +82,25 @@ public class MultiLineStringBuilder extends ShapeBuilder 0) { + sb.append(ShapeBuilder.coordinateListToWKT(lines.get(0).coordinates)); + } + for (int i = 1; i < lines.size(); ++i) { + sb.append(GeoWKTParser.COMMA); + sb.append(ShapeBuilder.coordinateListToWKT(lines.get(i).coordinates)); + } + sb.append(GeoWKTParser.RPAREN); + } + return sb; + } + @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); diff --git a/core/src/main/java/org/elasticsearch/common/geo/builders/MultiPolygonBuilder.java b/core/src/main/java/org/elasticsearch/common/geo/builders/MultiPolygonBuilder.java index 3c002631b8d..aa577887e00 100644 --- a/core/src/main/java/org/elasticsearch/common/geo/builders/MultiPolygonBuilder.java +++ b/core/src/main/java/org/elasticsearch/common/geo/builders/MultiPolygonBuilder.java @@ -21,6 +21,7 @@ package org.elasticsearch.common.geo.builders; import org.elasticsearch.common.geo.GeoShapeType; import org.elasticsearch.common.geo.parsers.ShapeParser; +import org.elasticsearch.common.geo.parsers.GeoWKTParser; import org.locationtech.spatial4j.shape.Shape; import com.vividsolutions.jts.geom.Coordinate; @@ -101,6 +102,37 @@ public class MultiPolygonBuilder extends ShapeBuilder { return polygons; } + private static String polygonCoordinatesToWKT(PolygonBuilder polygon) { + StringBuilder sb = new StringBuilder(); + sb.append(GeoWKTParser.LPAREN); + sb.append(ShapeBuilder.coordinateListToWKT(polygon.shell().coordinates)); + for (LineStringBuilder hole : polygon.holes()) { + sb.append(GeoWKTParser.COMMA); + sb.append(ShapeBuilder.coordinateListToWKT(hole.coordinates)); + } + sb.append(GeoWKTParser.RPAREN); + return sb.toString(); + } + + @Override + protected StringBuilder contentToWKT() { + final StringBuilder sb = new StringBuilder(); + if (polygons.isEmpty()) { + sb.append(GeoWKTParser.EMPTY); + } else { + sb.append(GeoWKTParser.LPAREN); + if (polygons.size() > 0) { + sb.append(polygonCoordinatesToWKT(polygons.get(0))); + } + for (int i = 1; i < polygons.size(); ++i) { + sb.append(GeoWKTParser.COMMA); + sb.append(polygonCoordinatesToWKT(polygons.get(i))); + } + sb.append(GeoWKTParser.RPAREN); + } + return sb; + } + @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); diff --git a/core/src/main/java/org/elasticsearch/common/geo/builders/PolygonBuilder.java b/core/src/main/java/org/elasticsearch/common/geo/builders/PolygonBuilder.java index 919aae37c73..ffcb44c9e46 100644 --- a/core/src/main/java/org/elasticsearch/common/geo/builders/PolygonBuilder.java +++ b/core/src/main/java/org/elasticsearch/common/geo/builders/PolygonBuilder.java @@ -729,6 +729,19 @@ public class PolygonBuilder extends ShapeBuilder { } } + @Override + protected StringBuilder contentToWKT() { + StringBuilder sb = new StringBuilder(); + sb.append('('); + sb.append(ShapeBuilder.coordinateListToWKT(shell.coordinates)); + for (LineStringBuilder hole : holes) { + sb.append(", "); + sb.append(ShapeBuilder.coordinateListToWKT(hole.coordinates)); + } + sb.append(')'); + return sb; + } + @Override public int hashCode() { return Objects.hash(shell, holes, orientation); diff --git a/core/src/main/java/org/elasticsearch/common/geo/builders/ShapeBuilder.java b/core/src/main/java/org/elasticsearch/common/geo/builders/ShapeBuilder.java index ef50a667faa..106c312a3bc 100644 --- a/core/src/main/java/org/elasticsearch/common/geo/builders/ShapeBuilder.java +++ b/core/src/main/java/org/elasticsearch/common/geo/builders/ShapeBuilder.java @@ -27,6 +27,7 @@ import org.apache.logging.log4j.Logger; import org.elasticsearch.Assertions; import org.elasticsearch.common.Strings; import org.elasticsearch.common.geo.GeoShapeType; +import org.elasticsearch.common.geo.parsers.GeoWKTParser; import org.elasticsearch.common.io.stream.NamedWriteable; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; @@ -339,6 +340,47 @@ public abstract class ShapeBuilder> } } + protected StringBuilder contentToWKT() { + return coordinateListToWKT(this.coordinates); + } + + public String toWKT() { + StringBuilder sb = new StringBuilder(); + sb.append(type().wktName()); + sb.append(GeoWKTParser.SPACE); + sb.append(contentToWKT()); + return sb.toString(); + } + + protected static StringBuilder coordinateListToWKT(final List coordinates) { + final StringBuilder sb = new StringBuilder(); + + if (coordinates.isEmpty()) { + sb.append(GeoWKTParser.EMPTY); + } else { + // walk through coordinates: + sb.append(GeoWKTParser.LPAREN); + sb.append(coordinateToWKT(coordinates.get(0))); + for (int i = 1; i < coordinates.size(); ++i) { + sb.append(GeoWKTParser.COMMA); + sb.append(GeoWKTParser.SPACE); + sb.append(coordinateToWKT(coordinates.get(i))); + } + sb.append(GeoWKTParser.RPAREN); + } + + return sb; + } + + private static String coordinateToWKT(final Coordinate coordinate) { + final StringBuilder sb = new StringBuilder(); + sb.append(coordinate.x + GeoWKTParser.SPACE + coordinate.y); + if (Double.isNaN(coordinate.z) == false) { + sb.append(GeoWKTParser.SPACE + coordinate.z); + } + return sb.toString(); + } + protected static final IntersectionOrder INTERSECTION_ORDER = new IntersectionOrder(); private static final class IntersectionOrder implements Comparator { diff --git a/core/src/main/java/org/elasticsearch/common/geo/parsers/GeoWKTParser.java b/core/src/main/java/org/elasticsearch/common/geo/parsers/GeoWKTParser.java new file mode 100644 index 00000000000..005caed53a7 --- /dev/null +++ b/core/src/main/java/org/elasticsearch/common/geo/parsers/GeoWKTParser.java @@ -0,0 +1,321 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch 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.elasticsearch.common.geo.parsers; + +import com.vividsolutions.jts.geom.Coordinate; +import org.elasticsearch.ElasticsearchParseException; +import org.elasticsearch.common.geo.GeoShapeType; + +import org.elasticsearch.common.geo.builders.CoordinatesBuilder; +import org.elasticsearch.common.geo.builders.EnvelopeBuilder; +import org.elasticsearch.common.geo.builders.GeometryCollectionBuilder; +import org.elasticsearch.common.geo.builders.LineStringBuilder; +import org.elasticsearch.common.geo.builders.MultiLineStringBuilder; +import org.elasticsearch.common.geo.builders.MultiPointBuilder; +import org.elasticsearch.common.geo.builders.MultiPolygonBuilder; +import org.elasticsearch.common.geo.builders.PointBuilder; +import org.elasticsearch.common.geo.builders.PolygonBuilder; +import org.elasticsearch.common.geo.builders.ShapeBuilder; +import org.elasticsearch.common.io.FastStringReader; +import org.elasticsearch.common.logging.Loggers; +import org.elasticsearch.common.xcontent.XContentParser; + +import java.io.IOException; +import java.io.StreamTokenizer; +import java.util.List; + +/** + * 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 GeoWKTParser { + public static final String EMPTY = "EMPTY"; + public static final String SPACE = Loggers.SPACE; + public static final String LPAREN = "("; + public static final String RPAREN = ")"; + public static final String COMMA = ","; + private 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 GeoWKTParser() {} + + public static ShapeBuilder parse(XContentParser parser) + throws IOException, ElasticsearchParseException { + FastStringReader reader = new FastStringReader(parser.text()); + try { + // 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('#'); + ShapeBuilder builder = parseGeometry(tokenizer); + checkEOF(tokenizer); + return builder; + } finally { + reader.close(); + } + } + + /** parse geometry from the stream tokenizer */ + private static ShapeBuilder parseGeometry(StreamTokenizer stream) throws IOException, ElasticsearchParseException { + final GeoShapeType type = GeoShapeType.forName(nextWord(stream)); + 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); + } + } + + private static EnvelopeBuilder parseBBox(StreamTokenizer stream) throws IOException, ElasticsearchParseException { + 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 EnvelopeBuilder(new Coordinate(minLon, maxLat), new Coordinate(maxLon, minLat)); + } + + private static PointBuilder parsePoint(StreamTokenizer stream) throws IOException, ElasticsearchParseException { + if (nextEmptyOrOpen(stream).equals(EMPTY)) { + return null; + } + PointBuilder pt = new PointBuilder(nextNumber(stream), nextNumber(stream)); + if (isNumberNext(stream) == true) { + nextNumber(stream); + } + nextCloser(stream); + return pt; + } + + private static List parseCoordinateList(StreamTokenizer stream) + throws IOException, ElasticsearchParseException { + CoordinatesBuilder coordinates = new CoordinatesBuilder(); + boolean isOpenParen = false; + if (isNumberNext(stream) || (isOpenParen = nextWord(stream).equals(LPAREN))) { + coordinates.coordinate(parseCoordinate(stream)); + } + + if (isOpenParen && nextCloser(stream).equals(RPAREN) == false) { + throw new ElasticsearchParseException("expected: [{}]" + RPAREN + " but found: [{}]" + tokenString(stream), stream.lineno()); + } + + while (nextCloserOrComma(stream).equals(COMMA)) { + isOpenParen = false; + if (isNumberNext(stream) || (isOpenParen = nextWord(stream).equals(LPAREN))) { + coordinates.coordinate(parseCoordinate(stream)); + } + if (isOpenParen && nextCloser(stream).equals(RPAREN) == false) { + throw new ElasticsearchParseException("expected: " + RPAREN + " but found: " + tokenString(stream), stream.lineno()); + } + } + return coordinates.build(); + } + + private static Coordinate parseCoordinate(StreamTokenizer stream) + throws IOException, ElasticsearchParseException { + final double lon = nextNumber(stream); + final double lat = nextNumber(stream); + Double z = null; + if (isNumberNext(stream)) { + z = nextNumber(stream); + } + return z == null ? new Coordinate(lon, lat) : new Coordinate(lon, lat, z); + } + + private static MultiPointBuilder parseMultiPoint(StreamTokenizer stream) throws IOException, ElasticsearchParseException { + String token = nextEmptyOrOpen(stream); + if (token.equals(EMPTY)) { + return null; + } + return new MultiPointBuilder(parseCoordinateList(stream)); + } + + private static LineStringBuilder parseLine(StreamTokenizer stream) throws IOException, ElasticsearchParseException { + String token = nextEmptyOrOpen(stream); + if (token.equals(EMPTY)) { + return null; + } + return new LineStringBuilder(parseCoordinateList(stream)); + } + + private static MultiLineStringBuilder parseMultiLine(StreamTokenizer stream) throws IOException, ElasticsearchParseException { + String token = nextEmptyOrOpen(stream); + if (token.equals(EMPTY)) { + return null; + } + MultiLineStringBuilder builder = new MultiLineStringBuilder(); + builder.linestring(parseLine(stream)); + while (nextCloserOrComma(stream).equals(COMMA)) { + builder.linestring(parseLine(stream)); + } + return builder; + } + + private static PolygonBuilder parsePolygon(StreamTokenizer stream) throws IOException, ElasticsearchParseException { + if (nextEmptyOrOpen(stream).equals(EMPTY)) { + return null; + } + PolygonBuilder builder = new PolygonBuilder(parseLine(stream), ShapeBuilder.Orientation.RIGHT); + while (nextCloserOrComma(stream).equals(COMMA)) { + builder.hole(parseLine(stream)); + } + return builder; + } + + private static MultiPolygonBuilder parseMultiPolygon(StreamTokenizer stream) throws IOException, ElasticsearchParseException { + if (nextEmptyOrOpen(stream).equals(EMPTY)) { + return null; + } + MultiPolygonBuilder builder = new MultiPolygonBuilder().polygon(parsePolygon(stream)); + while (nextCloserOrComma(stream).equals(COMMA)) { + builder.polygon(parsePolygon(stream)); + } + return builder; + } + + private static GeometryCollectionBuilder parseGeometryCollection(StreamTokenizer stream) + throws IOException, ElasticsearchParseException { + if (nextEmptyOrOpen(stream).equals(EMPTY)) { + return null; + } + GeometryCollectionBuilder builder = new GeometryCollectionBuilder().shape(parseGeometry(stream)); + while (nextCloserOrComma(stream).equals(COMMA)) { + builder.shape(parseGeometry(stream)); + } + return builder; + } + + /** next word in the stream */ + private static String nextWord(StreamTokenizer stream) throws ElasticsearchParseException, 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 ElasticsearchParseException("expected word but found: " + tokenString(stream), stream.lineno()); + } + + private static double nextNumber(StreamTokenizer stream) throws IOException, ElasticsearchParseException { + 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 ElasticsearchParseException("invalid number found: " + stream.sval, stream.lineno()); + } + } + } + throw new ElasticsearchParseException("expected number but found: " + tokenString(stream), stream.lineno()); + } + + 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 + "'"; + } + + private static boolean isNumberNext(StreamTokenizer stream) throws IOException { + final int type = stream.nextToken(); + stream.pushBack(); + return type == StreamTokenizer.TT_WORD; + } + + private static String nextEmptyOrOpen(StreamTokenizer stream) throws IOException, ElasticsearchParseException { + final String next = nextWord(stream); + if (next.equals(EMPTY) || next.equals(LPAREN)) { + return next; + } + throw new ElasticsearchParseException("expected " + EMPTY + " or " + LPAREN + + " but found: " + tokenString(stream), stream.lineno()); + } + + private static String nextCloser(StreamTokenizer stream) throws IOException, ElasticsearchParseException { + if (nextWord(stream).equals(RPAREN)) { + return RPAREN; + } + throw new ElasticsearchParseException("expected " + RPAREN + " but found: " + tokenString(stream), stream.lineno()); + } + + private static String nextComma(StreamTokenizer stream) throws IOException, ElasticsearchParseException { + if (nextWord(stream).equals(COMMA) == true) { + return COMMA; + } + throw new ElasticsearchParseException("expected " + COMMA + " but found: " + tokenString(stream), stream.lineno()); + } + + private static String nextCloserOrComma(StreamTokenizer stream) throws IOException, ElasticsearchParseException { + String token = nextWord(stream); + if (token.equals(COMMA) || token.equals(RPAREN)) { + return token; + } + throw new ElasticsearchParseException("expected " + COMMA + " or " + RPAREN + + " but found: " + tokenString(stream), stream.lineno()); + } + + /** next word in the stream */ + private static void checkEOF(StreamTokenizer stream) throws ElasticsearchParseException, IOException { + if (stream.nextToken() != StreamTokenizer.TT_EOF) { + throw new ElasticsearchParseException("expected end of WKT string but found additional text: " + + tokenString(stream), stream.lineno()); + } + } +} diff --git a/core/src/main/java/org/elasticsearch/common/geo/parsers/ShapeParser.java b/core/src/main/java/org/elasticsearch/common/geo/parsers/ShapeParser.java index 39540f902fe..0ee3333c480 100644 --- a/core/src/main/java/org/elasticsearch/common/geo/parsers/ShapeParser.java +++ b/core/src/main/java/org/elasticsearch/common/geo/parsers/ShapeParser.java @@ -51,6 +51,8 @@ public interface ShapeParser { return null; } if (parser.currentToken() == XContentParser.Token.START_OBJECT) { return GeoJsonParser.parse(parser, shapeMapper); + } else if (parser.currentToken() == XContentParser.Token.VALUE_STRING) { + return GeoWKTParser.parse(parser); } throw new ElasticsearchParseException("shape must be an object consisting of type and coordinates"); } diff --git a/core/src/test/java/org/elasticsearch/common/geo/BaseGeoParsingTestCase.java b/core/src/test/java/org/elasticsearch/common/geo/BaseGeoParsingTestCase.java index 3297e956e60..fff415de555 100644 --- a/core/src/test/java/org/elasticsearch/common/geo/BaseGeoParsingTestCase.java +++ b/core/src/test/java/org/elasticsearch/common/geo/BaseGeoParsingTestCase.java @@ -18,10 +18,21 @@ */ package org.elasticsearch.common.geo; +import com.vividsolutions.jts.geom.Geometry; import com.vividsolutions.jts.geom.GeometryFactory; +import org.elasticsearch.common.geo.parsers.ShapeParser; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.hamcrest.ElasticsearchGeoAssertions; +import org.locationtech.spatial4j.shape.Shape; +import org.locationtech.spatial4j.shape.ShapeCollection; +import org.locationtech.spatial4j.shape.jts.JtsGeometry; import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import static org.elasticsearch.common.geo.builders.ShapeBuilder.SPATIAL_CONTEXT; @@ -35,4 +46,35 @@ abstract class BaseGeoParsingTestCase extends ESTestCase { public abstract void testParseMultiLineString() throws IOException; public abstract void testParsePolygon() throws IOException; public abstract void testParseMultiPolygon() throws IOException; + public abstract void testParseEnvelope() throws IOException; + public abstract void testParseGeometryCollection() throws IOException; + + protected void assertValidException(XContentBuilder builder, Class expectedException) throws IOException { + XContentParser parser = createParser(builder); + parser.nextToken(); + ElasticsearchGeoAssertions.assertValidException(parser, expectedException); + } + + protected void assertGeometryEquals(Shape expected, XContentBuilder geoJson) throws IOException { + XContentParser parser = createParser(geoJson); + parser.nextToken(); + ElasticsearchGeoAssertions.assertEquals(expected, ShapeParser.parse(parser).build()); + } + + protected ShapeCollection shapeCollection(Shape... shapes) { + return new ShapeCollection<>(Arrays.asList(shapes), SPATIAL_CONTEXT); + } + + protected ShapeCollection shapeCollection(Geometry... geoms) { + List shapes = new ArrayList<>(geoms.length); + for (Geometry geom : geoms) { + shapes.add(jtsGeom(geom)); + } + return new ShapeCollection<>(shapes, SPATIAL_CONTEXT); + } + + protected JtsGeometry jtsGeom(Geometry geom) { + return new JtsGeometry(geom, SPATIAL_CONTEXT, false, false); + } + } diff --git a/core/src/test/java/org/elasticsearch/common/geo/GeoJsonShapeParserTests.java b/core/src/test/java/org/elasticsearch/common/geo/GeoJsonShapeParserTests.java index 32f384d96b1..fc987c7e3ca 100644 --- a/core/src/test/java/org/elasticsearch/common/geo/GeoJsonShapeParserTests.java +++ b/core/src/test/java/org/elasticsearch/common/geo/GeoJsonShapeParserTests.java @@ -20,7 +20,6 @@ package org.elasticsearch.common.geo; import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.Geometry; import com.vividsolutions.jts.geom.LineString; import com.vividsolutions.jts.geom.LinearRing; import com.vividsolutions.jts.geom.MultiLineString; @@ -39,12 +38,10 @@ import org.locationtech.spatial4j.shape.Circle; import org.locationtech.spatial4j.shape.Rectangle; import org.locationtech.spatial4j.shape.Shape; import org.locationtech.spatial4j.shape.ShapeCollection; -import org.locationtech.spatial4j.shape.jts.JtsGeometry; import org.locationtech.spatial4j.shape.jts.JtsPoint; import java.io.IOException; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import static org.elasticsearch.common.geo.builders.ShapeBuilder.SPATIAL_CONTEXT; @@ -159,6 +156,7 @@ public class GeoJsonShapeParserTests extends BaseGeoParsingTestCase { assertGeometryEquals(jtsGeom(expectedLS), lineGeoJson); } + @Override public void testParseEnvelope() throws IOException { // test #1: envelope with expected coordinate order (TopLeft, BottomRight) XContentBuilder multilinesGeoJson = XContentFactory.jsonBuilder().startObject().field("type", "envelope") @@ -1033,27 +1031,4 @@ public class GeoJsonShapeParserTests extends BaseGeoParsingTestCase { ElasticsearchGeoAssertions.assertMultiPolygon(shape); } - - private void assertGeometryEquals(Shape expected, XContentBuilder geoJson) throws IOException { - XContentParser parser = createParser(geoJson); - parser.nextToken(); - ElasticsearchGeoAssertions.assertEquals(expected, ShapeParser.parse(parser).build()); - } - - private ShapeCollection shapeCollection(Shape... shapes) { - return new ShapeCollection<>(Arrays.asList(shapes), SPATIAL_CONTEXT); - } - - private ShapeCollection shapeCollection(Geometry... geoms) { - List shapes = new ArrayList<>(geoms.length); - for (Geometry geom : geoms) { - shapes.add(jtsGeom(geom)); - } - return new ShapeCollection<>(shapes, SPATIAL_CONTEXT); - } - - private JtsGeometry jtsGeom(Geometry geom) { - return new JtsGeometry(geom, SPATIAL_CONTEXT, false, false); - } - } diff --git a/core/src/test/java/org/elasticsearch/common/geo/GeoWKTShapeParserTests.java b/core/src/test/java/org/elasticsearch/common/geo/GeoWKTShapeParserTests.java new file mode 100644 index 00000000000..191ce702052 --- /dev/null +++ b/core/src/test/java/org/elasticsearch/common/geo/GeoWKTShapeParserTests.java @@ -0,0 +1,255 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch 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.elasticsearch.common.geo; + +import com.vividsolutions.jts.geom.Coordinate; +import com.vividsolutions.jts.geom.LineString; +import com.vividsolutions.jts.geom.LinearRing; +import com.vividsolutions.jts.geom.MultiLineString; +import com.vividsolutions.jts.geom.Point; +import com.vividsolutions.jts.geom.Polygon; +import org.apache.lucene.geo.GeoTestUtil; +import org.elasticsearch.ElasticsearchParseException; +import org.elasticsearch.common.geo.builders.CoordinatesBuilder; +import org.elasticsearch.common.geo.builders.EnvelopeBuilder; +import org.elasticsearch.common.geo.builders.GeometryCollectionBuilder; +import org.elasticsearch.common.geo.builders.LineStringBuilder; +import org.elasticsearch.common.geo.builders.MultiLineStringBuilder; +import org.elasticsearch.common.geo.builders.MultiPointBuilder; +import org.elasticsearch.common.geo.builders.MultiPolygonBuilder; +import org.elasticsearch.common.geo.builders.PointBuilder; +import org.elasticsearch.common.geo.builders.PolygonBuilder; +import org.elasticsearch.common.geo.builders.ShapeBuilder; +import org.elasticsearch.common.geo.parsers.GeoWKTParser; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.test.geo.RandomShapeGenerator; +import org.locationtech.spatial4j.exception.InvalidShapeException; +import org.locationtech.spatial4j.shape.Rectangle; +import org.locationtech.spatial4j.shape.Shape; +import org.locationtech.spatial4j.shape.ShapeCollection; +import org.locationtech.spatial4j.shape.jts.JtsPoint; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import static org.elasticsearch.common.geo.builders.ShapeBuilder.SPATIAL_CONTEXT; + +/** + * Tests for {@code GeoWKTShapeParser} + */ +public class GeoWKTShapeParserTests extends BaseGeoParsingTestCase { + + private static XContentBuilder toWKTContent(ShapeBuilder builder, boolean generateMalformed) + throws IOException { + String wkt = builder.toWKT(); + if (generateMalformed) { + // malformed - extra paren + // TODO generate more malformed WKT + wkt += GeoWKTParser.RPAREN; + } + if (randomBoolean()) { + // test comments + wkt = "# " + wkt + "\n" + wkt; + } + return XContentFactory.jsonBuilder().value(wkt); + } + + private void assertExpected(Shape expected, ShapeBuilder builder) throws IOException { + XContentBuilder xContentBuilder = toWKTContent(builder, false); + assertGeometryEquals(expected, xContentBuilder); + } + + private void assertMalformed(Shape expected, ShapeBuilder builder) throws IOException { + XContentBuilder xContentBuilder = toWKTContent(builder, true); + assertValidException(xContentBuilder, ElasticsearchParseException.class); + } + + @Override + public void testParsePoint() throws IOException { + GeoPoint p = RandomShapeGenerator.randomPoint(random()); + Coordinate c = new Coordinate(p.lon(), p.lat()); + Point expected = GEOMETRY_FACTORY.createPoint(c); + assertExpected(new JtsPoint(expected, SPATIAL_CONTEXT), new PointBuilder().coordinate(c)); + assertMalformed(new JtsPoint(expected, SPATIAL_CONTEXT), new PointBuilder().coordinate(c)); + } + + @Override + public void testParseMultiPoint() throws IOException { + int numPoints = randomIntBetween(2, 100); + List coordinates = new ArrayList<>(numPoints); + Shape[] shapes = new Shape[numPoints]; + GeoPoint p = new GeoPoint(); + for (int i = 0; i < numPoints; ++i) { + p.reset(GeoTestUtil.nextLatitude(), GeoTestUtil.nextLongitude()); + coordinates.add(new Coordinate(p.lon(), p.lat())); + shapes[i] = SPATIAL_CONTEXT.makePoint(p.lon(), p.lat()); + } + ShapeCollection expected = shapeCollection(shapes); + assertExpected(expected, new MultiPointBuilder(coordinates)); + assertMalformed(expected, new MultiPointBuilder(coordinates)); + } + + private List randomLineStringCoords() { + int numPoints = randomIntBetween(2, 100); + List coordinates = new ArrayList<>(numPoints); + GeoPoint p; + for (int i = 0; i < numPoints; ++i) { + p = RandomShapeGenerator.randomPointIn(random(), -90d, -90d, 90d, 90d); + coordinates.add(new Coordinate(p.lon(), p.lat())); + } + return coordinates; + } + + @Override + public void testParseLineString() throws IOException { + List coordinates = randomLineStringCoords(); + LineString expected = GEOMETRY_FACTORY.createLineString(coordinates.toArray(new Coordinate[coordinates.size()])); + assertExpected(jtsGeom(expected), new LineStringBuilder(coordinates)); + } + + @Override + public void testParseMultiLineString() throws IOException { + int numLineStrings = randomIntBetween(2, 8); + List lineStrings = new ArrayList<>(numLineStrings); + MultiLineStringBuilder builder = new MultiLineStringBuilder(); + for (int j = 0; j < numLineStrings; ++j) { + List lsc = randomLineStringCoords(); + Coordinate [] coords = lsc.toArray(new Coordinate[lsc.size()]); + lineStrings.add(GEOMETRY_FACTORY.createLineString(coords)); + builder.linestring(new LineStringBuilder(lsc)); + } + MultiLineString expected = GEOMETRY_FACTORY.createMultiLineString( + lineStrings.toArray(new LineString[lineStrings.size()])); + assertExpected(jtsGeom(expected), builder); + assertMalformed(jtsGeom(expected), builder); + } + + @Override + public void testParsePolygon() throws IOException { + PolygonBuilder builder = PolygonBuilder.class.cast( + RandomShapeGenerator.createShape(random(), RandomShapeGenerator.ShapeType.POLYGON)); + Coordinate[] coords = builder.coordinates()[0][0]; + LinearRing shell = GEOMETRY_FACTORY.createLinearRing(coords); + Polygon expected = GEOMETRY_FACTORY.createPolygon(shell, null); + assertExpected(jtsGeom(expected), builder); + assertMalformed(jtsGeom(expected), builder); + } + + @Override + public void testParseMultiPolygon() throws IOException { + int numPolys = randomIntBetween(2, 8); + MultiPolygonBuilder builder = new MultiPolygonBuilder(); + PolygonBuilder pb; + Coordinate[] coordinates; + Polygon[] shapes = new Polygon[numPolys]; + LinearRing shell; + for (int i = 0; i < numPolys; ++i) { + pb = PolygonBuilder.class.cast(RandomShapeGenerator.createShape(random(), RandomShapeGenerator.ShapeType.POLYGON)); + builder.polygon(pb); + coordinates = pb.coordinates()[0][0]; + shell = GEOMETRY_FACTORY.createLinearRing(coordinates); + shapes[i] = GEOMETRY_FACTORY.createPolygon(shell, null); + } + Shape expected = shapeCollection(shapes); + assertExpected(expected, builder); + assertMalformed(expected, builder); + } + + public void testParsePolygonWithHole() throws IOException { + // add 3d point to test ISSUE #10501 + List shellCoordinates = new ArrayList<>(); + shellCoordinates.add(new Coordinate(100, 0, 15.0)); + shellCoordinates.add(new Coordinate(101, 0)); + shellCoordinates.add(new Coordinate(101, 1)); + shellCoordinates.add(new Coordinate(100, 1, 10.0)); + shellCoordinates.add(new Coordinate(100, 0)); + + List holeCoordinates = new ArrayList<>(); + holeCoordinates.add(new Coordinate(100.2, 0.2)); + holeCoordinates.add(new Coordinate(100.8, 0.2)); + holeCoordinates.add(new Coordinate(100.8, 0.8)); + holeCoordinates.add(new Coordinate(100.2, 0.8)); + holeCoordinates.add(new Coordinate(100.2, 0.2)); + + PolygonBuilder polygonWithHole = new PolygonBuilder(new CoordinatesBuilder().coordinates(shellCoordinates)); + polygonWithHole.hole(new LineStringBuilder(holeCoordinates)); + + LinearRing shell = GEOMETRY_FACTORY.createLinearRing( + shellCoordinates.toArray(new Coordinate[shellCoordinates.size()])); + LinearRing[] holes = new LinearRing[1]; + holes[0] = GEOMETRY_FACTORY.createLinearRing( + holeCoordinates.toArray(new Coordinate[holeCoordinates.size()])); + Polygon expected = GEOMETRY_FACTORY.createPolygon(shell, holes); + + assertExpected(jtsGeom(expected), polygonWithHole); + assertMalformed(jtsGeom(expected), polygonWithHole); + } + + public void testParseSelfCrossingPolygon() throws IOException { + // test self crossing ccw poly not crossing dateline + List shellCoordinates = new ArrayList<>(); + shellCoordinates.add(new Coordinate(176, 15)); + shellCoordinates.add(new Coordinate(-177, 10)); + shellCoordinates.add(new Coordinate(-177, -10)); + shellCoordinates.add(new Coordinate(176, -15)); + shellCoordinates.add(new Coordinate(-177, 15)); + shellCoordinates.add(new Coordinate(172, 0)); + shellCoordinates.add(new Coordinate(176, 15)); + + PolygonBuilder poly = new PolygonBuilder(new CoordinatesBuilder().coordinates(shellCoordinates)); + XContentBuilder builder = XContentFactory.jsonBuilder().value(poly.toWKT()); + assertValidException(builder, InvalidShapeException.class); + } + + public void testMalformedWKT() throws IOException { + // malformed points in a polygon is a common typo + String malformedWKT = "POLYGON ((100, 5) (100, 10) (90, 10), (90, 5), (100, 5)"; + XContentBuilder builder = XContentFactory.jsonBuilder().value(malformedWKT); + assertValidException(builder, ElasticsearchParseException.class); + } + + @Override + public void testParseEnvelope() throws IOException { + org.apache.lucene.geo.Rectangle r = GeoTestUtil.nextBox(); + EnvelopeBuilder builder = new EnvelopeBuilder(new Coordinate(r.minLon, r.maxLat), new Coordinate(r.maxLon, r.minLat)); + Rectangle expected = SPATIAL_CONTEXT.makeRectangle(r.minLon, r.maxLon, r.minLat, r.maxLat); + assertExpected(expected, builder); + assertMalformed(expected, builder); + } + + public void testInvalidGeometryType() throws IOException { + XContentBuilder builder = XContentFactory.jsonBuilder().value("UnknownType (-1 -2)"); + assertValidException(builder, IllegalArgumentException.class); + } + + @Override + public void testParseGeometryCollection() throws IOException { + if (rarely()) { + // assert empty shape collection + GeometryCollectionBuilder builder = new GeometryCollectionBuilder(); + Shape[] expected = new Shape[0]; + assertEquals(shapeCollection(expected).isEmpty(), builder.build().isEmpty()); + } else { + GeometryCollectionBuilder gcb = RandomShapeGenerator.createGeometryCollection(random()); + assertExpected(gcb.build(), gcb); + } + } +} diff --git a/docs/reference/mapping/types/geo-shape.asciidoc b/docs/reference/mapping/types/geo-shape.asciidoc index b3420dbb58a..23caaf6a8ec 100644 --- a/docs/reference/mapping/types/geo-shape.asciidoc +++ b/docs/reference/mapping/types/geo-shape.asciidoc @@ -74,7 +74,7 @@ different ways. 1. Right-hand rule: `right`, `ccw`, `counterclockwise`, outer ring vertices in counterclockwise order with inner ring(s) vertices (holes) in clockwise order. Setting this parameter in the geo_shape mapping explicitly sets vertex order for the coordinate list of a geo_shape field but can be -overridden in each individual GeoJSON document. +overridden in each individual GeoJSON or WKT document. | `ccw` |`points_only` |Setting this option to `true` (defaults to `false`) configures @@ -86,8 +86,9 @@ by improving point performance on a `geo_shape` field so that `geo_shape` querie optimal on a point only field. | `false` -|`ignore_malformed` |If true, malformed geojson shapes are ignored. If false (default), -malformed geojson shapes throw an exception and reject the whole document. +|`ignore_malformed` |If true, malformed GeoJSON or WKT shapes are ignored. If +false (default), malformed GeoJSON and WKT shapes throw an exception and reject the +entire document. | `false` @@ -204,29 +205,30 @@ overly bloating the resulting index too much relative to the input size. [float] ==== Input Structure -The http://www.geojson.org[GeoJSON] format is used to represent -http://geojson.org/geojson-spec.html#geometry-objects[shapes] as input -as follows: +Shapes can be represented using either the http://www.geojson.org[GeoJSON] +or http://docs.opengeospatial.org/is/12-063r5/12-063r5.html[Well-Known Text] +(WKT) format. The following table provides a mapping of GeoJSON and WKT +to Elasticsearch types: -[cols="<,<,<",options="header",] +[cols="<,<,<,<",options="header",] |======================================================================= -|GeoJSON Type |Elasticsearch Type |Description +|GeoJSON Type |WKT Type |Elasticsearch Type |Description -|`Point` |`point` |A single geographic coordinate. -|`LineString` |`linestring` |An arbitrary line given two or more points. -|`Polygon` |`polygon` |A _closed_ polygon whose first and last point +|`Point` |`POINT` |`point` |A single geographic coordinate. +|`LineString` |`LINESTRING` |`linestring` |An arbitrary line given two or more points. +|`Polygon` |`POLYGON` |`polygon` |A _closed_ polygon whose first and last point must match, thus requiring `n + 1` vertices to create an `n`-sided polygon and a minimum of `4` vertices. -|`MultiPoint` |`multipoint` |An array of unconnected, but likely related +|`MultiPoint` |`MULTIPOINT` |`multipoint` |An array of unconnected, but likely related points. -|`MultiLineString` |`multilinestring` |An array of separate linestrings. -|`MultiPolygon` |`multipolygon` |An array of separate polygons. -|`GeometryCollection` |`geometrycollection` | A GeoJSON shape similar to the +|`MultiLineString` |`MULTILINESTRING` |`multilinestring` |An array of separate linestrings. +|`MultiPolygon` |`MULTIPOLYGON` |`multipolygon` |An array of separate polygons. +|`GeometryCollection` |`GEOMETRYCOLLECTION` |`geometrycollection` | A GeoJSON shape similar to the `multi*` shapes except that multiple types can coexist (e.g., a Point and a LineString). -|`N/A` |`envelope` |A bounding rectangle, or envelope, specified by +|`N/A` |`BBOX` |`envelope` |A bounding rectangle, or envelope, specified by specifying only the top left and bottom right points. -|`N/A` |`circle` |A circle specified by a center point and radius with +|`N/A` |`N/A` |`circle` |A circle specified by a center point and radius with units, which default to `METERS`. |======================================================================= @@ -235,7 +237,7 @@ units, which default to `METERS`. For all types, both the inner `type` and `coordinates` fields are required. -In GeoJSON, and therefore Elasticsearch, the correct *coordinate +In GeoJSON and WKT, and therefore Elasticsearch, the correct *coordinate order is longitude, latitude (X, Y)* within coordinate arrays. This differs from many Geospatial APIs (e.g., Google Maps) that generally use the colloquial latitude, longitude (Y, X). @@ -247,7 +249,7 @@ use the colloquial latitude, longitude (Y, X). A point is a single geographic coordinate, such as the location of a building or the current position given by a smartphone's Geolocation -API. +API. The following is an example of a point in GeoJSON. [source,js] -------------------------------------------------- @@ -261,12 +263,24 @@ POST /example/doc -------------------------------------------------- // CONSOLE +The following is an example of a point in WKT: + +[source,js] +-------------------------------------------------- +POST /example/doc +{ + "location" : "POINT (-77.03653 38.897676)" +} +-------------------------------------------------- +// CONSOLE + [float] ===== http://geojson.org/geojson-spec.html#id3[LineString] A `linestring` defined by an array of two or more positions. By specifying only two points, the `linestring` will represent a straight -line. Specifying more than two points creates an arbitrary path. +line. Specifying more than two points creates an arbitrary path. The +following is an example of a LineString in GeoJSON. [source,js] -------------------------------------------------- @@ -280,6 +294,17 @@ POST /example/doc -------------------------------------------------- // CONSOLE +The following is an example of a LineString in WKT: + +[source,js] +-------------------------------------------------- +POST /example/doc +{ + "location" : "LINESTRING (-77.03653 38.897676, -77.009051 38.889939)" +} +-------------------------------------------------- +// CONSOLE + The above `linestring` would draw a straight line starting at the White House to the US Capitol Building. @@ -288,7 +313,7 @@ House to the US Capitol Building. A polygon is defined by a list of a list of points. The first and last points in each (outer) list must be the same (the polygon must be -closed). +closed). The following is an example of a Polygon in GeoJSON. [source,js] -------------------------------------------------- @@ -304,8 +329,20 @@ POST /example/doc -------------------------------------------------- // CONSOLE +The following is an example of a Polygon in WKT: + +[source,js] +-------------------------------------------------- +POST /example/doc +{ + "location" : "POLYGON ((100.0 0.0, 101.0 0.0, 101.0 1.0, 100.0 1.0, 100.0 0.0))" +} +-------------------------------------------------- +// CONSOLE + The first array represents the outer boundary of the polygon, the other -arrays represent the interior shapes ("holes"): +arrays represent the interior shapes ("holes"). The following is a GeoJSON example +of a polygon with a hole: [source,js] -------------------------------------------------- @@ -323,9 +360,21 @@ POST /example/doc // CONSOLE // TEST[skip:https://github.com/elastic/elasticsearch/issues/23836] -*IMPORTANT NOTE:* GeoJSON does not mandate a specific order for vertices thus ambiguous -polygons around the dateline and poles are possible. To alleviate ambiguity -the Open Geospatial Consortium (OGC) +The following is an example of a Polygon with a hole in WKT: + +[source,js] +-------------------------------------------------- +POST /example/doc +{ + "location" : "POLYGON ((100.0 0.0, 101.0 0.0, 101.0 1.0, 100.0 1.0, 100.0 0.0), (100.2 0.2, 100.8 0.2, 100.8 0.8, 100.2 0.8, 100.2 0.2))" +} +-------------------------------------------------- +// CONSOLE +// TEST[skip:https://github.com/elastic/elasticsearch/issues/23836] + +*IMPORTANT NOTE:* GeoJSON and WKT do not enforce a specific order for vertices +thus ambiguous polygons around the dateline and poles are possible. To alleviate +ambiguity the Open Geospatial Consortium (OGC) http://www.opengeospatial.org/standards/sfa[Simple Feature Access] specification defines the following vertex ordering: @@ -380,7 +429,7 @@ POST /example/doc [float] ===== http://www.geojson.org/geojson-spec.html#id5[MultiPoint] -A list of geojson points. +The following is an example of a list of geojson points: [source,js] -------------------------------------------------- @@ -396,10 +445,21 @@ POST /example/doc -------------------------------------------------- // CONSOLE +The following is an example of a list of WKT points: + +[source,js] +-------------------------------------------------- +POST /example/doc +{ + "location" : "MULTIPOINT (102.0 2.0, 103.0 2.0)" +} +-------------------------------------------------- +// CONSOLE + [float] ===== http://www.geojson.org/geojson-spec.html#id6[MultiLineString] -A list of geojson linestrings. +The following is an example of a list of geojson linestrings: [source,js] -------------------------------------------------- @@ -418,10 +478,22 @@ POST /example/doc // CONSOLE // TEST[skip:https://github.com/elastic/elasticsearch/issues/23836] +The following is an example of a list of WKT linestrings: + +[source,js] +-------------------------------------------------- +POST /example/doc +{ + "location" : "MULTILINESTRING ((102.0 2.0, 103.0 2.0, 103.0 3.0, 102.0 3.0), (100.0 0.0, 101.0 0.0, 101.0 1.0, 100.0 1.0), (100.2 0.2, 100.8 0.2, 100.8 0.8, 100.2 0.8))" +} +-------------------------------------------------- +// CONSOLE +// TEST[skip:https://github.com/elastic/elasticsearch/issues/23836] + [float] ===== http://www.geojson.org/geojson-spec.html#id7[MultiPolygon] -A list of geojson polygons. +The following is an example of a list of geojson polygons (second polygon contains a hole): [source,js] -------------------------------------------------- @@ -440,10 +512,22 @@ POST /example/doc // CONSOLE // TEST[skip:https://github.com/elastic/elasticsearch/issues/23836] +The following is an example of a list of WKT polygons (second polygon contains a hole): + +[source,js] +-------------------------------------------------- +POST /example/doc +{ + "location" : "MULTIPOLYGON (((102.0 2.0, 103.0 2.0, 103.0 3.0, 102.0 3.0, 102.0 2.0)), ((100.0 0.0, 101.0 0.0, 101.0 1.0, 100.0 1.0, 100.0 0.0), (100.2 0.2, 100.8 0.2, 100.8 0.8, 100.2 0.8, 100.2 0.2)))" +} +-------------------------------------------------- +// CONSOLE +// TEST[skip:https://github.com/elastic/elasticsearch/issues/23836] + [float] ===== http://geojson.org/geojson-spec.html#geometrycollection[Geometry Collection] -A collection of geojson geometry objects. +The following is an example of a collection of geojson geometry objects: [source,js] -------------------------------------------------- @@ -467,6 +551,19 @@ POST /example/doc // CONSOLE // TEST[skip:https://github.com/elastic/elasticsearch/issues/23836] +The following is an example of a collection of WKT geometry objects: + +[source,js] +-------------------------------------------------- +POST /example/doc +{ + "location" : "GEOMETRYCOLLECTION (POINT (100.0 0.0), LINESTRING (101.0 0.0, 102.0 1.0))" +} +-------------------------------------------------- +// CONSOLE +// TEST[skip:https://github.com/elastic/elasticsearch/issues/23836] + + [float] ===== Envelope @@ -487,6 +584,20 @@ POST /example/doc // CONSOLE // TEST[skip:https://github.com/elastic/elasticsearch/issues/23836] +The following is an example of an envelope using the WKT BBOX format: + +*NOTE:* WKT specification expects the following order: minLon, maxLon, maxLat, minLat. + +[source,js] +-------------------------------------------------- +POST /example/doc +{ + "location" : "BBOX (-45.0, 45.0, 45.0, -45.0)" +} +-------------------------------------------------- +// CONSOLE +// TEST[skip:https://github.com/elastic/elasticsearch/issues/23836] + [float] ===== Circle @@ -509,6 +620,8 @@ POST /example/doc Note: The inner `radius` field is required. If not specified, then the units of the `radius` will default to `METERS`. +*NOTE:* Neither GeoJSON or WKT support a point-radius circle type. + [float] ==== Sorting and Retrieving index Shapes