mirror of
https://github.com/honeymoose/OpenSearch.git
synced 2025-02-17 10:25:15 +00:00
Geo: enables coerce support in WKT polygon parser (#35414)
WKT parser now automatically closes open polygons similar to GeoJSON parser if coerce flag in mapping is set to true. Closes to #35059
This commit is contained in:
parent
16cbbab7b7
commit
e7896bcefc
@ -19,6 +19,7 @@
|
|||||||
package org.elasticsearch.common.geo.parsers;
|
package org.elasticsearch.common.geo.parsers;
|
||||||
|
|
||||||
import org.elasticsearch.ElasticsearchParseException;
|
import org.elasticsearch.ElasticsearchParseException;
|
||||||
|
import org.elasticsearch.common.Explicit;
|
||||||
import org.elasticsearch.common.geo.GeoPoint;
|
import org.elasticsearch.common.geo.GeoPoint;
|
||||||
import org.elasticsearch.common.geo.GeoShapeType;
|
import org.elasticsearch.common.geo.GeoShapeType;
|
||||||
import org.elasticsearch.common.geo.builders.CoordinatesBuilder;
|
import org.elasticsearch.common.geo.builders.CoordinatesBuilder;
|
||||||
@ -77,7 +78,9 @@ public class GeoWKTParser {
|
|||||||
final GeoShapeFieldMapper shapeMapper)
|
final GeoShapeFieldMapper shapeMapper)
|
||||||
throws IOException, ElasticsearchParseException {
|
throws IOException, ElasticsearchParseException {
|
||||||
try (StringReader reader = new StringReader(parser.text())) {
|
try (StringReader reader = new StringReader(parser.text())) {
|
||||||
boolean ignoreZValue = (shapeMapper != null && shapeMapper.ignoreZValue().value() == true);
|
Explicit<Boolean> ignoreZValue = (shapeMapper == null) ? GeoShapeFieldMapper.Defaults.IGNORE_Z_VALUE :
|
||||||
|
shapeMapper.ignoreZValue();
|
||||||
|
Explicit<Boolean> coerce = (shapeMapper == null) ? GeoShapeFieldMapper.Defaults.COERCE : shapeMapper.coerce();
|
||||||
// setup the tokenizer; configured to read words w/o numbers
|
// setup the tokenizer; configured to read words w/o numbers
|
||||||
StreamTokenizer tokenizer = new StreamTokenizer(reader);
|
StreamTokenizer tokenizer = new StreamTokenizer(reader);
|
||||||
tokenizer.resetSyntax();
|
tokenizer.resetSyntax();
|
||||||
@ -90,14 +93,15 @@ public class GeoWKTParser {
|
|||||||
tokenizer.wordChars('.', '.');
|
tokenizer.wordChars('.', '.');
|
||||||
tokenizer.whitespaceChars(0, ' ');
|
tokenizer.whitespaceChars(0, ' ');
|
||||||
tokenizer.commentChar('#');
|
tokenizer.commentChar('#');
|
||||||
ShapeBuilder builder = parseGeometry(tokenizer, shapeType, ignoreZValue);
|
ShapeBuilder builder = parseGeometry(tokenizer, shapeType, ignoreZValue.value(), coerce.value());
|
||||||
checkEOF(tokenizer);
|
checkEOF(tokenizer);
|
||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** parse geometry from the stream tokenizer */
|
/** parse geometry from the stream tokenizer */
|
||||||
private static ShapeBuilder parseGeometry(StreamTokenizer stream, GeoShapeType shapeType, final boolean ignoreZValue)
|
private static ShapeBuilder parseGeometry(StreamTokenizer stream, GeoShapeType shapeType, final boolean ignoreZValue,
|
||||||
|
final boolean coerce)
|
||||||
throws IOException, ElasticsearchParseException {
|
throws IOException, ElasticsearchParseException {
|
||||||
final GeoShapeType type = GeoShapeType.forName(nextWord(stream));
|
final GeoShapeType type = GeoShapeType.forName(nextWord(stream));
|
||||||
if (shapeType != null && shapeType != GeoShapeType.GEOMETRYCOLLECTION) {
|
if (shapeType != null && shapeType != GeoShapeType.GEOMETRYCOLLECTION) {
|
||||||
@ -107,21 +111,21 @@ public class GeoWKTParser {
|
|||||||
}
|
}
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case POINT:
|
case POINT:
|
||||||
return parsePoint(stream, ignoreZValue);
|
return parsePoint(stream, ignoreZValue, coerce);
|
||||||
case MULTIPOINT:
|
case MULTIPOINT:
|
||||||
return parseMultiPoint(stream, ignoreZValue);
|
return parseMultiPoint(stream, ignoreZValue, coerce);
|
||||||
case LINESTRING:
|
case LINESTRING:
|
||||||
return parseLine(stream, ignoreZValue);
|
return parseLine(stream, ignoreZValue, coerce);
|
||||||
case MULTILINESTRING:
|
case MULTILINESTRING:
|
||||||
return parseMultiLine(stream, ignoreZValue);
|
return parseMultiLine(stream, ignoreZValue, coerce);
|
||||||
case POLYGON:
|
case POLYGON:
|
||||||
return parsePolygon(stream, ignoreZValue);
|
return parsePolygon(stream, ignoreZValue, coerce);
|
||||||
case MULTIPOLYGON:
|
case MULTIPOLYGON:
|
||||||
return parseMultiPolygon(stream, ignoreZValue);
|
return parseMultiPolygon(stream, ignoreZValue, coerce);
|
||||||
case ENVELOPE:
|
case ENVELOPE:
|
||||||
return parseBBox(stream);
|
return parseBBox(stream);
|
||||||
case GEOMETRYCOLLECTION:
|
case GEOMETRYCOLLECTION:
|
||||||
return parseGeometryCollection(stream, ignoreZValue);
|
return parseGeometryCollection(stream, ignoreZValue, coerce);
|
||||||
default:
|
default:
|
||||||
throw new IllegalArgumentException("Unknown geometry type: " + type);
|
throw new IllegalArgumentException("Unknown geometry type: " + type);
|
||||||
}
|
}
|
||||||
@ -142,7 +146,7 @@ public class GeoWKTParser {
|
|||||||
return new EnvelopeBuilder(new Coordinate(minLon, maxLat), new Coordinate(maxLon, minLat));
|
return new EnvelopeBuilder(new Coordinate(minLon, maxLat), new Coordinate(maxLon, minLat));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static PointBuilder parsePoint(StreamTokenizer stream, final boolean ignoreZValue)
|
private static PointBuilder parsePoint(StreamTokenizer stream, final boolean ignoreZValue, final boolean coerce)
|
||||||
throws IOException, ElasticsearchParseException {
|
throws IOException, ElasticsearchParseException {
|
||||||
if (nextEmptyOrOpen(stream).equals(EMPTY)) {
|
if (nextEmptyOrOpen(stream).equals(EMPTY)) {
|
||||||
return null;
|
return null;
|
||||||
@ -155,12 +159,12 @@ public class GeoWKTParser {
|
|||||||
return pt;
|
return pt;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<Coordinate> parseCoordinateList(StreamTokenizer stream, final boolean ignoreZValue)
|
private static List<Coordinate> parseCoordinateList(StreamTokenizer stream, final boolean ignoreZValue, final boolean coerce)
|
||||||
throws IOException, ElasticsearchParseException {
|
throws IOException, ElasticsearchParseException {
|
||||||
CoordinatesBuilder coordinates = new CoordinatesBuilder();
|
CoordinatesBuilder coordinates = new CoordinatesBuilder();
|
||||||
boolean isOpenParen = false;
|
boolean isOpenParen = false;
|
||||||
if (isNumberNext(stream) || (isOpenParen = nextWord(stream).equals(LPAREN))) {
|
if (isNumberNext(stream) || (isOpenParen = nextWord(stream).equals(LPAREN))) {
|
||||||
coordinates.coordinate(parseCoordinate(stream, ignoreZValue));
|
coordinates.coordinate(parseCoordinate(stream, ignoreZValue, coerce));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isOpenParen && nextCloser(stream).equals(RPAREN) == false) {
|
if (isOpenParen && nextCloser(stream).equals(RPAREN) == false) {
|
||||||
@ -170,7 +174,7 @@ public class GeoWKTParser {
|
|||||||
while (nextCloserOrComma(stream).equals(COMMA)) {
|
while (nextCloserOrComma(stream).equals(COMMA)) {
|
||||||
isOpenParen = false;
|
isOpenParen = false;
|
||||||
if (isNumberNext(stream) || (isOpenParen = nextWord(stream).equals(LPAREN))) {
|
if (isNumberNext(stream) || (isOpenParen = nextWord(stream).equals(LPAREN))) {
|
||||||
coordinates.coordinate(parseCoordinate(stream, ignoreZValue));
|
coordinates.coordinate(parseCoordinate(stream, ignoreZValue, coerce));
|
||||||
}
|
}
|
||||||
if (isOpenParen && nextCloser(stream).equals(RPAREN) == false) {
|
if (isOpenParen && nextCloser(stream).equals(RPAREN) == false) {
|
||||||
throw new ElasticsearchParseException("expected: " + RPAREN + " but found: " + tokenString(stream), stream.lineno());
|
throw new ElasticsearchParseException("expected: " + RPAREN + " but found: " + tokenString(stream), stream.lineno());
|
||||||
@ -179,7 +183,7 @@ public class GeoWKTParser {
|
|||||||
return coordinates.build();
|
return coordinates.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Coordinate parseCoordinate(StreamTokenizer stream, final boolean ignoreZValue)
|
private static Coordinate parseCoordinate(StreamTokenizer stream, final boolean ignoreZValue, final boolean coerce)
|
||||||
throws IOException, ElasticsearchParseException {
|
throws IOException, ElasticsearchParseException {
|
||||||
final double lon = nextNumber(stream);
|
final double lon = nextNumber(stream);
|
||||||
final double lat = nextNumber(stream);
|
final double lat = nextNumber(stream);
|
||||||
@ -190,71 +194,98 @@ public class GeoWKTParser {
|
|||||||
return z == null ? new Coordinate(lon, lat) : new Coordinate(lon, lat, z);
|
return z == null ? new Coordinate(lon, lat) : new Coordinate(lon, lat, z);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static MultiPointBuilder parseMultiPoint(StreamTokenizer stream, final boolean ignoreZValue)
|
private static MultiPointBuilder parseMultiPoint(StreamTokenizer stream, final boolean ignoreZValue, final boolean coerce)
|
||||||
throws IOException, ElasticsearchParseException {
|
throws IOException, ElasticsearchParseException {
|
||||||
String token = nextEmptyOrOpen(stream);
|
String token = nextEmptyOrOpen(stream);
|
||||||
if (token.equals(EMPTY)) {
|
if (token.equals(EMPTY)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return new MultiPointBuilder(parseCoordinateList(stream, ignoreZValue));
|
return new MultiPointBuilder(parseCoordinateList(stream, ignoreZValue, coerce));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static LineStringBuilder parseLine(StreamTokenizer stream, final boolean ignoreZValue)
|
private static LineStringBuilder parseLine(StreamTokenizer stream, final boolean ignoreZValue, final boolean coerce)
|
||||||
throws IOException, ElasticsearchParseException {
|
throws IOException, ElasticsearchParseException {
|
||||||
String token = nextEmptyOrOpen(stream);
|
String token = nextEmptyOrOpen(stream);
|
||||||
if (token.equals(EMPTY)) {
|
if (token.equals(EMPTY)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return new LineStringBuilder(parseCoordinateList(stream, ignoreZValue));
|
return new LineStringBuilder(parseCoordinateList(stream, ignoreZValue, coerce));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static MultiLineStringBuilder parseMultiLine(StreamTokenizer stream, final boolean ignoreZValue)
|
// A LinearRing is closed LineString with 4 or more positions. The first and last positions
|
||||||
|
// are equivalent (they represent equivalent points).
|
||||||
|
private static LineStringBuilder parseLinearRing(StreamTokenizer stream, final boolean ignoreZValue, final boolean coerce)
|
||||||
|
throws IOException, ElasticsearchParseException {
|
||||||
|
String token = nextEmptyOrOpen(stream);
|
||||||
|
if (token.equals(EMPTY)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
List<Coordinate> coordinates = parseCoordinateList(stream, ignoreZValue, coerce);
|
||||||
|
int coordinatesNeeded = coerce ? 3 : 4;
|
||||||
|
if (coordinates.size() >= coordinatesNeeded) {
|
||||||
|
if (!coordinates.get(0).equals(coordinates.get(coordinates.size() - 1))) {
|
||||||
|
if (coerce == true) {
|
||||||
|
coordinates.add(coordinates.get(0));
|
||||||
|
} else {
|
||||||
|
throw new ElasticsearchParseException("invalid LinearRing found (coordinates are not closed)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (coordinates.size() < 4) {
|
||||||
|
throw new ElasticsearchParseException("invalid number of points in LinearRing (found [{}] - must be >= 4)",
|
||||||
|
coordinates.size());
|
||||||
|
}
|
||||||
|
return new LineStringBuilder(coordinates);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MultiLineStringBuilder parseMultiLine(StreamTokenizer stream, final boolean ignoreZValue, final boolean coerce)
|
||||||
throws IOException, ElasticsearchParseException {
|
throws IOException, ElasticsearchParseException {
|
||||||
String token = nextEmptyOrOpen(stream);
|
String token = nextEmptyOrOpen(stream);
|
||||||
if (token.equals(EMPTY)) {
|
if (token.equals(EMPTY)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
MultiLineStringBuilder builder = new MultiLineStringBuilder();
|
MultiLineStringBuilder builder = new MultiLineStringBuilder();
|
||||||
builder.linestring(parseLine(stream, ignoreZValue));
|
builder.linestring(parseLine(stream, ignoreZValue, coerce));
|
||||||
while (nextCloserOrComma(stream).equals(COMMA)) {
|
while (nextCloserOrComma(stream).equals(COMMA)) {
|
||||||
builder.linestring(parseLine(stream, ignoreZValue));
|
builder.linestring(parseLine(stream, ignoreZValue, coerce));
|
||||||
}
|
}
|
||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static PolygonBuilder parsePolygon(StreamTokenizer stream, final boolean ignoreZValue)
|
private static PolygonBuilder parsePolygon(StreamTokenizer stream, final boolean ignoreZValue, final boolean coerce)
|
||||||
throws IOException, ElasticsearchParseException {
|
throws IOException, ElasticsearchParseException {
|
||||||
if (nextEmptyOrOpen(stream).equals(EMPTY)) {
|
if (nextEmptyOrOpen(stream).equals(EMPTY)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
PolygonBuilder builder = new PolygonBuilder(parseLine(stream, ignoreZValue), ShapeBuilder.Orientation.RIGHT);
|
PolygonBuilder builder = new PolygonBuilder(parseLinearRing(stream, ignoreZValue, coerce), ShapeBuilder.Orientation.RIGHT);
|
||||||
while (nextCloserOrComma(stream).equals(COMMA)) {
|
while (nextCloserOrComma(stream).equals(COMMA)) {
|
||||||
builder.hole(parseLine(stream, ignoreZValue));
|
builder.hole(parseLinearRing(stream, ignoreZValue, coerce));
|
||||||
}
|
}
|
||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static MultiPolygonBuilder parseMultiPolygon(StreamTokenizer stream, final boolean ignoreZValue)
|
private static MultiPolygonBuilder parseMultiPolygon(StreamTokenizer stream, final boolean ignoreZValue, final boolean coerce)
|
||||||
throws IOException, ElasticsearchParseException {
|
throws IOException, ElasticsearchParseException {
|
||||||
if (nextEmptyOrOpen(stream).equals(EMPTY)) {
|
if (nextEmptyOrOpen(stream).equals(EMPTY)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
MultiPolygonBuilder builder = new MultiPolygonBuilder().polygon(parsePolygon(stream, ignoreZValue));
|
MultiPolygonBuilder builder = new MultiPolygonBuilder().polygon(parsePolygon(stream, ignoreZValue, coerce));
|
||||||
while (nextCloserOrComma(stream).equals(COMMA)) {
|
while (nextCloserOrComma(stream).equals(COMMA)) {
|
||||||
builder.polygon(parsePolygon(stream, ignoreZValue));
|
builder.polygon(parsePolygon(stream, ignoreZValue, coerce));
|
||||||
}
|
}
|
||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static GeometryCollectionBuilder parseGeometryCollection(StreamTokenizer stream, final boolean ignoreZValue)
|
private static GeometryCollectionBuilder parseGeometryCollection(StreamTokenizer stream, final boolean ignoreZValue,
|
||||||
|
final boolean coerce)
|
||||||
throws IOException, ElasticsearchParseException {
|
throws IOException, ElasticsearchParseException {
|
||||||
if (nextEmptyOrOpen(stream).equals(EMPTY)) {
|
if (nextEmptyOrOpen(stream).equals(EMPTY)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
GeometryCollectionBuilder builder = new GeometryCollectionBuilder().shape(
|
GeometryCollectionBuilder builder = new GeometryCollectionBuilder().shape(
|
||||||
parseGeometry(stream, GeoShapeType.GEOMETRYCOLLECTION, ignoreZValue));
|
parseGeometry(stream, GeoShapeType.GEOMETRYCOLLECTION, ignoreZValue, coerce));
|
||||||
while (nextCloserOrComma(stream).equals(COMMA)) {
|
while (nextCloserOrComma(stream).equals(COMMA)) {
|
||||||
builder.shape(parseGeometry(stream, null, ignoreZValue));
|
builder.shape(parseGeometry(stream, null, ignoreZValue, coerce));
|
||||||
}
|
}
|
||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
|
@ -135,7 +135,7 @@ public class GeoShapeFieldMapper extends FieldMapper {
|
|||||||
|
|
||||||
public Builder coerce(boolean coerce) {
|
public Builder coerce(boolean coerce) {
|
||||||
this.coerce = coerce;
|
this.coerce = coerce;
|
||||||
return builder;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -155,7 +155,7 @@ public class GeoShapeFieldMapper extends FieldMapper {
|
|||||||
|
|
||||||
public Builder ignoreMalformed(boolean ignoreMalformed) {
|
public Builder ignoreMalformed(boolean ignoreMalformed) {
|
||||||
this.ignoreMalformed = ignoreMalformed;
|
this.ignoreMalformed = ignoreMalformed;
|
||||||
return builder;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Explicit<Boolean> ignoreMalformed(BuilderContext context) {
|
protected Explicit<Boolean> ignoreMalformed(BuilderContext context) {
|
||||||
|
@ -318,6 +318,31 @@ public class GeoWKTShapeParserTests extends BaseGeoParsingTestCase {
|
|||||||
assertEquals(shapeBuilder.numDimensions(), 3);
|
assertEquals(shapeBuilder.numDimensions(), 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void testParseOpenPolygon() throws IOException {
|
||||||
|
String openPolygon = "POLYGON ((100 5, 100 10, 90 10, 90 5))";
|
||||||
|
|
||||||
|
XContentBuilder xContentBuilder = XContentFactory.jsonBuilder().value(openPolygon);
|
||||||
|
XContentParser parser = createParser(xContentBuilder);
|
||||||
|
parser.nextToken();
|
||||||
|
|
||||||
|
Settings indexSettings = Settings.builder()
|
||||||
|
.put(IndexMetaData.SETTING_VERSION_CREATED, Version.V_6_3_0)
|
||||||
|
.put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, 0)
|
||||||
|
.put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, 1)
|
||||||
|
.put(IndexMetaData.SETTING_INDEX_UUID, UUIDs.randomBase64UUID()).build();
|
||||||
|
|
||||||
|
Mapper.BuilderContext mockBuilderContext = new Mapper.BuilderContext(indexSettings, new ContentPath());
|
||||||
|
final GeoShapeFieldMapper defaultMapperBuilder = new GeoShapeFieldMapper.Builder("test").coerce(false).build(mockBuilderContext);
|
||||||
|
ElasticsearchParseException exception = expectThrows(ElasticsearchParseException.class,
|
||||||
|
() -> ShapeParser.parse(parser, defaultMapperBuilder));
|
||||||
|
assertEquals("invalid LinearRing found (coordinates are not closed)", exception.getMessage());
|
||||||
|
|
||||||
|
final GeoShapeFieldMapper coercingMapperBuilder = new GeoShapeFieldMapper.Builder("test").coerce(true).build(mockBuilderContext);
|
||||||
|
ShapeBuilder<?, ?> shapeBuilder = ShapeParser.parse(parser, coercingMapperBuilder);
|
||||||
|
assertNotNull(shapeBuilder);
|
||||||
|
assertEquals("polygon ((100.0 5.0, 100.0 10.0, 90.0 10.0, 90.0 5.0, 100.0 5.0))", shapeBuilder.toWKT());
|
||||||
|
}
|
||||||
|
|
||||||
public void testParseSelfCrossingPolygon() throws IOException {
|
public void testParseSelfCrossingPolygon() throws IOException {
|
||||||
// test self crossing ccw poly not crossing dateline
|
// test self crossing ccw poly not crossing dateline
|
||||||
List<Coordinate> shellCoordinates = new ArrayList<>();
|
List<Coordinate> shellCoordinates = new ArrayList<>();
|
||||||
|
Loading…
x
Reference in New Issue
Block a user