diff --git a/libs/geo/src/main/java/org/elasticsearch/geometry/Point.java b/libs/geo/src/main/java/org/elasticsearch/geometry/Point.java index dc6a29db87bc0..bb390ad2d5cd1 100644 --- a/libs/geo/src/main/java/org/elasticsearch/geometry/Point.java +++ b/libs/geo/src/main/java/org/elasticsearch/geometry/Point.java @@ -16,6 +16,8 @@ */ public class Point implements Geometry { public static final Point EMPTY = new Point(); + public static final String Z = "Z"; + public static final String M = "M"; private final double y; private final double x; diff --git a/libs/geo/src/main/java/org/elasticsearch/geometry/utils/WellKnownText.java b/libs/geo/src/main/java/org/elasticsearch/geometry/utils/WellKnownText.java index 9e657013a80dd..345e511454ad2 100644 --- a/libs/geo/src/main/java/org/elasticsearch/geometry/utils/WellKnownText.java +++ b/libs/geo/src/main/java/org/elasticsearch/geometry/utils/WellKnownText.java @@ -250,7 +250,6 @@ private static void parseGeometry(ByteBuffer byteBuffer, StringBuilder sb) { case 1018 -> parseBBox(byteBuffer, true, sb); default -> throw new IllegalArgumentException("Unknown geometry type: " + type); } - ; } private static void writeCoordinate(ByteBuffer byteBuffer, boolean hasZ, StringBuilder sb) { @@ -472,15 +471,23 @@ private static GeometryCollection parseGeometryCollection(StreamTokeni } private static Point parsePoint(StreamTokenizer stream) throws IOException, ParseException { - if (nextEmptyOrOpen(stream).equals(EMPTY)) { + if (isEmptyNext(stream)) { return Point.EMPTY; } + String token = nextZOrMOrOpen(stream); + boolean hasZOrM = token.equals(Point.Z) || token.equals(Point.M); + if (hasZOrM) { + nextOpener(stream); + } double lon = nextNumber(stream); double lat = nextNumber(stream); Point pt; if (isNumberNext(stream)) { pt = new Point(lon, lat, nextNumber(stream)); } else { + if (hasZOrM) { + throw new ParseException("'POINT Z' or 'POINT M' must have three coordinates, but only two were found.", stream.lineno()); + } pt = new Point(lon, lat); } nextCloser(stream); @@ -710,6 +717,14 @@ private static boolean isNumberNext(StreamTokenizer stream) throws IOException { return type == StreamTokenizer.TT_WORD; } + private static boolean isEmptyNext(StreamTokenizer stream) throws ParseException, IOException { + if (nextWord(stream).equals(EMPTY)) { + return true; + } + stream.pushBack(); + return false; + } + private static String nextEmptyOrOpen(StreamTokenizer stream) throws IOException, ParseException { final String next = nextWord(stream); if (next.equals(EMPTY) || next.equals(LPAREN)) { @@ -718,6 +733,17 @@ private static String nextEmptyOrOpen(StreamTokenizer stream) throws IOException throw new ParseException("expected " + EMPTY + " or " + LPAREN + " but found: " + tokenString(stream), stream.lineno()); } + private static String nextZOrMOrOpen(StreamTokenizer stream) throws ParseException, IOException { + final String next = nextWord(stream); + if (next.equals(Point.Z) || next.equals(Point.M) || next.equals(LPAREN)) { + return next; + } + throw new ParseException( + "expected " + LPAREN + " or " + Point.Z + " or " + Point.M + " but found: " + tokenString(stream), + stream.lineno() + ); + } + private static String nextCloser(StreamTokenizer stream) throws IOException, ParseException { if (nextWord(stream).equals(RPAREN)) { return RPAREN; diff --git a/libs/geo/src/test/java/org/elasticsearch/geometry/PointTests.java b/libs/geo/src/test/java/org/elasticsearch/geometry/PointTests.java index b61c36579bfca..56db8c2050217 100644 --- a/libs/geo/src/test/java/org/elasticsearch/geometry/PointTests.java +++ b/libs/geo/src/test/java/org/elasticsearch/geometry/PointTests.java @@ -17,6 +17,7 @@ import java.io.IOException; import java.text.ParseException; +import java.util.List; public class PointTests extends BaseGeometryTestCase { @Override @@ -58,6 +59,52 @@ public void testWKTValidation() { assertEquals("found Z value [100.0] but [ignore_z_value] parameter is [false]", ex.getMessage()); } + public void testParsePointZWithThreeCoordinates() throws IOException, ParseException { + GeometryValidator validator = GeographyValidator.instance(true); + assertEquals(new Point(20, 10, 100), WellKnownText.fromWKT(validator, true, "POINT Z (20.0 10.0 100.0)")); + } + + public void testParsePointMWithThreeCoordinates() throws IOException, ParseException { + GeometryValidator validator = GeographyValidator.instance(true); + assertEquals(new Point(20, 10, 100), WellKnownText.fromWKT(validator, true, "POINT M (20.0 10.0 100.0)")); + } + + public void testParsePointZWithTwoCoordinatesThrowsException() { + GeometryValidator validator = GeographyValidator.instance(true); + ParseException ex = expectThrows(ParseException.class, () -> WellKnownText.fromWKT(validator, true, "POINT Z (20.0 10.0)")); + assertEquals("'POINT Z' or 'POINT M' must have three coordinates, but only two were found.", ex.getMessage()); + } + + public void testParsePointMWithTwoCoordinatesThrowsException() { + GeometryValidator validator = StandardValidator.instance(true); + ParseException ex = expectThrows(ParseException.class, () -> WellKnownText.fromWKT(validator, true, "POINT M (20.0 10.0)")); + assertEquals("'POINT Z' or 'POINT M' must have three coordinates, but only two were found.", ex.getMessage()); + } + + public void testParsePointZMFormatNotSupported() { + GeometryValidator validator = StandardValidator.instance(true); + List points = List.of("POINT ZM (20.0 10.0 100.0 200.0)", "POINT ZM (20.0 10.0 100.0)", "POINT ZM (20.0 10.0)"); + for (String point : points) { + ParseException ex = expectThrows(ParseException.class, () -> WellKnownText.fromWKT(validator, true, point)); + assertEquals("expected ( or Z or M but found: ZM", ex.getMessage()); + } + } + + public void testParsePointZWithEmpty() { + GeometryValidator validator = StandardValidator.instance(true); + ParseException ex = expectThrows(ParseException.class, () -> WellKnownText.fromWKT(validator, true, "POINT Z EMPTY")); + assertEquals("expected ( but found: EMPTY", ex.getMessage()); + } + + public void testParsePointZOrMWithTwoCoordinates() { + GeometryValidator validator = StandardValidator.instance(true); + List points = List.of("POINT Z (20.0 10.0)", "POINT M (20.0 10.0)"); + for (String point : points) { + ParseException ex = expectThrows(ParseException.class, () -> WellKnownText.fromWKT(validator, true, point)); + assertEquals("'POINT Z' or 'POINT M' must have three coordinates, but only two were found.", ex.getMessage()); + } + } + @Override protected Point mutateInstance(Point instance) { return null;// TODO implement https://github.com/elastic/elasticsearch/issues/25929