Support explicit Z/M attributes using WKT geometry (#125896)

This commit is contained in:
Omri Cohen 2025-04-03 18:00:12 +03:00 committed by GitHub
parent 30b2a1f729
commit 856ee3a177
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 271 additions and 1 deletions

View file

@ -0,0 +1,5 @@
pr: 125896
summary: Support explicit Z/M attributes using WKT geometry
area: Geo
type: enhancement
issues: [123111]

View file

@ -44,6 +44,8 @@ public class WellKnownText {
public static final String RPAREN = ")";
public static final String COMMA = ",";
public static final String NAN = "NaN";
public static final String Z = "Z";
public static final String M = "M";
public static final int MAX_NESTED_DEPTH = 1000;
private static final String NUMBER = "<NUMBER>";
@ -440,7 +442,8 @@ public class WellKnownText {
*/
private static Geometry parseGeometry(StreamTokenizer stream, boolean coerce, int depth) throws IOException, ParseException {
final String type = nextWord(stream).toLowerCase(Locale.ROOT);
return switch (type) {
final boolean isExplicitlySpecifiesZorM = isZOrMNext(stream);
Geometry geometry = switch (type) {
case "point" -> parsePoint(stream);
case "multipoint" -> parseMultiPoint(stream);
case "linestring" -> parseLine(stream);
@ -453,6 +456,16 @@ public class WellKnownText {
parseCircle(stream);
default -> throw new IllegalArgumentException("Unknown geometry type: " + type);
};
checkZorMAttribute(isExplicitlySpecifiesZorM, geometry.hasZ());
return geometry;
}
private static void checkZorMAttribute(boolean isExplicitlySpecifiesZorM, boolean hasZ) {
if (isExplicitlySpecifiesZorM && hasZ == false) {
throw new IllegalArgumentException(
"When specifying 'Z' or 'M', coordinates must include three values. Only two coordinates were provided"
);
}
}
private static GeometryCollection<Geometry> parseGeometryCollection(StreamTokenizer stream, boolean coerce, int depth)
@ -710,6 +723,21 @@ public class WellKnownText {
return type == StreamTokenizer.TT_WORD;
}
private static boolean isZOrMNext(StreamTokenizer stream) {
String token;
try {
token = nextWord(stream);
if (token.equals(Z) || token.equals(M)) {
return true;
}
stream.pushBack();
return false;
} catch (ParseException | IOException e) {
return false;
}
}
private static String nextEmptyOrOpen(StreamTokenizer stream) throws IOException, ParseException {
final String next = nextWord(stream);
if (next.equals(EMPTY) || next.equals(LPAREN)) {

View file

@ -21,6 +21,9 @@ import java.util.concurrent.atomic.AtomicBoolean;
abstract class BaseGeometryTestCase<T extends Geometry> extends AbstractWireTestCase<T> {
public static final String ZorMMustIncludeThreeValuesMsg =
"When specifying 'Z' or 'M', coordinates must include three values. Only two coordinates were provided";
@Override
protected final T createTestInstance() {
boolean hasAlt = randomBoolean();

View file

@ -19,6 +19,7 @@ import java.io.IOException;
import java.text.ParseException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import static org.hamcrest.Matchers.containsString;
@ -93,6 +94,35 @@ public class GeometryCollectionTests extends BaseGeometryTestCase<GeometryCollec
return count;
}
public void testParseGeometryCollectionZorMWithThreeCoordinates() throws IOException, ParseException {
GeometryValidator validator = GeographyValidator.instance(true);
GeometryCollection<Geometry> expected = new GeometryCollection<>(
Arrays.asList(
new Point(20.0, 10.0, 100.0),
new Line(new double[] { 10.0, 20.0 }, new double[] { 5.0, 15.0 }, new double[] { 50.0, 150.0 })
)
);
String point = "(POINT Z (20.0 10.0 100.0)";
String lineString = "LINESTRING M (10.0 5.0 50.0, 20.0 15.0 150.0)";
assertEquals(expected, WellKnownText.fromWKT(validator, true, "GEOMETRYCOLLECTION Z " + point + ", " + lineString + ")"));
assertEquals(expected, WellKnownText.fromWKT(validator, true, "GEOMETRYCOLLECTION M " + point + ", " + lineString + ")"));
}
public void testParseGeometryCollectionZorMWithTwoCoordinatesThrowsException() {
GeometryValidator validator = GeographyValidator.instance(true);
List<String> gcWkt = List.of(
"GEOMETRYCOLLECTION Z (POINT (20.0 10.0), LINESTRING (10.0 5.0, 20.0 15.0))",
"GEOMETRYCOLLECTION M (POINT (20.0 10.0), LINESTRING (10.0 5.0, 20.0 15.0))"
);
for (String gc : gcWkt) {
IllegalArgumentException ex = expectThrows(IllegalArgumentException.class, () -> WellKnownText.fromWKT(validator, true, gc));
assertEquals(ZorMMustIncludeThreeValuesMsg, ex.getMessage());
}
}
@Override
protected GeometryCollection<Geometry> mutateInstance(GeometryCollection<Geometry> instance) {
return null;// TODO implement https://github.com/elastic/elasticsearch/issues/25929

View file

@ -17,6 +17,7 @@ import org.elasticsearch.geometry.utils.WellKnownText;
import java.io.IOException;
import java.text.ParseException;
import java.util.List;
public class LineTests extends BaseGeometryTestCase<Line> {
@Override
@ -82,6 +83,25 @@ public class LineTests extends BaseGeometryTestCase<Line> {
assertEquals("found Z value [6.0] but [ignore_z_value] parameter is [false]", ex.getMessage());
}
public void testParseLineZorMWithThreeCoordinates() throws IOException, ParseException {
GeometryValidator validator = GeographyValidator.instance(true);
Line expectedZ = new Line(new double[] { 20.0, 30.0 }, new double[] { 10.0, 15.0 }, new double[] { 100.0, 200.0 });
assertEquals(expectedZ, WellKnownText.fromWKT(validator, true, "LINESTRING Z (20.0 10.0 100.0, 30.0 15.0 200.0)"));
Line expectedM = new Line(new double[] { 20.0, 30.0 }, new double[] { 10.0, 15.0 }, new double[] { 100.0, 200.0 });
assertEquals(expectedM, WellKnownText.fromWKT(validator, true, "LINESTRING M (20.0 10.0 100.0, 30.0 15.0 200.0)"));
}
public void testParseLineZorMWithTwoCoordinatesThrowsException() {
GeometryValidator validator = GeographyValidator.instance(true);
List<String> linesWkt = List.of("LINESTRING Z (20.0 10.0, 30.0 15.0)", "LINESTRING M (20.0 10.0, 30.0 15.0)");
for (String line : linesWkt) {
IllegalArgumentException ex = expectThrows(IllegalArgumentException.class, () -> WellKnownText.fromWKT(validator, true, line));
assertEquals(ZorMMustIncludeThreeValuesMsg, ex.getMessage());
}
}
@Override
protected Line mutateInstance(Line instance) {
return null;// TODO implement https://github.com/elastic/elasticsearch/issues/25929

View file

@ -64,6 +64,54 @@ public class MultiLineTests extends BaseGeometryTestCase<MultiLine> {
);
}
public void testParseMultiLineZorMWithThreeCoordinates() throws IOException, ParseException {
GeometryValidator validator = GeographyValidator.instance(true);
MultiLine expectedZ = new MultiLine(
List.of(
new Line(new double[] { 20.0, 30.0 }, new double[] { 10.0, 15.0 }, new double[] { 100.0, 200.0 }),
new Line(new double[] { 40.0, 50.0 }, new double[] { 20.0, 25.0 }, new double[] { 300.0, 400.0 })
)
);
assertEquals(
expectedZ,
WellKnownText.fromWKT(
validator,
true,
"MULTILINESTRING Z ((20.0 10.0 100.0, 30.0 15.0 200.0), (40.0 20.0 300.0, 50.0 25.0 400.0))"
)
);
MultiLine expectedM = new MultiLine(
List.of(
new Line(new double[] { 20.0, 30.0 }, new double[] { 10.0, 15.0 }, new double[] { 100.0, 200.0 }),
new Line(new double[] { 40.0, 50.0 }, new double[] { 20.0, 25.0 }, new double[] { 300.0, 400.0 })
)
);
assertEquals(
expectedM,
WellKnownText.fromWKT(
validator,
true,
"MULTILINESTRING M ((20.0 10.0 100.0, 30.0 15.0 200.0), (40.0 20.0 300.0, 50.0 25.0 400.0))"
)
);
}
public void testParseMultiLineZorMWithTwoCoordinatesThrowsException() {
GeometryValidator validator = GeographyValidator.instance(true);
List<String> multiLinesWkt = List.of(
"MULTILINESTRING Z ((20.0 10.0, 30.0 15.0), (40.0 20.0, 50.0 25.0))",
"MULTILINESTRING M ((20.0 10.0, 30.0 15.0), (40.0 20.0, 50.0 25.0))"
);
for (String multiLine : multiLinesWkt) {
IllegalArgumentException ex = expectThrows(
IllegalArgumentException.class,
() -> WellKnownText.fromWKT(validator, true, multiLine)
);
assertEquals(ZorMMustIncludeThreeValuesMsg, ex.getMessage());
}
}
@Override
protected MultiLine mutateInstance(MultiLine instance) {
return null;// TODO implement https://github.com/elastic/elasticsearch/issues/25929

View file

@ -71,6 +71,27 @@ public class MultiPointTests extends BaseGeometryTestCase<MultiPoint> {
StandardValidator.instance(true).validate(new MultiPoint(Collections.singletonList(new Point(2, 1, 3))));
}
public void testParseMultiPointWithThreeCoordinates() throws IOException, ParseException {
GeometryValidator validator = GeographyValidator.instance(true);
MultiPoint expectedZ = new MultiPoint(Arrays.asList(new Point(10, 20, 30), new Point(40, 50, 60)));
MultiPoint expectedM = new MultiPoint(Arrays.asList(new Point(10, 20, 30), new Point(40, 50, 60)));
assertEquals(expectedZ, WellKnownText.fromWKT(validator, true, "MULTIPOINT Z (10 20 30, 40 50 60)"));
assertEquals(expectedM, WellKnownText.fromWKT(validator, true, "MULTIPOINT M (10 20 30, 40 50 60)"));
}
public void testParseMultiPointWithTwoCoordinatesThrowsException() {
GeometryValidator validator = GeographyValidator.instance(true);
List<String> multiPointsWkt = List.of("MULTIPOINT Z (10 20, 40 50)", "MULTIPOINT M (10 20, 40 50)");
for (String multiPoint : multiPointsWkt) {
IllegalArgumentException ex = expectThrows(
IllegalArgumentException.class,
() -> WellKnownText.fromWKT(validator, true, multiPoint)
);
assertEquals(ZorMMustIncludeThreeValuesMsg, ex.getMessage());
}
}
@Override
protected MultiPoint mutateInstance(MultiPoint instance) {
return null;// TODO implement https://github.com/elastic/elasticsearch/issues/25929

View file

@ -80,6 +80,48 @@ public class MultiPolygonTests extends BaseGeometryTestCase<MultiPolygon> {
);
}
public void testParseMultiPolygonZorMWithThreeCoordinates() throws IOException, ParseException {
GeometryValidator validator = GeographyValidator.instance(true);
MultiPolygon expected = new MultiPolygon(
List.of(
new Polygon(
new LinearRing(
new double[] { 20.0, 30.0, 40.0, 20.0 },
new double[] { 10.0, 15.0, 10.0, 10.0 },
new double[] { 100.0, 200.0, 300.0, 100.0 }
)
),
new Polygon(
new LinearRing(
new double[] { 0.0, 10.0, 10.0, 0.0 },
new double[] { 0.0, 0.0, 10.0, 0.0 },
new double[] { 10.0, 20.0, 30.0, 10.0 }
)
)
)
);
String polygonA = "(20.0 10.0 100.0, 30.0 15.0 200.0, 40.0 10.0 300.0, 20.0 10.0 100.0)";
String polygonB = "(0.0 0.0 10.0, 10.0 0.0 20.0, 10.0 10.0 30.0, 0.0 0.0 10.0)";
assertEquals(expected, WellKnownText.fromWKT(validator, true, "MULTIPOLYGON Z ((" + polygonA + "), (" + polygonB + "))"));
assertEquals(expected, WellKnownText.fromWKT(validator, true, "MULTIPOLYGON M ((" + polygonA + "), (" + polygonB + "))"));
}
public void testParseMultiPolygonZorMWithTwoCoordinatesThrowsException() {
GeometryValidator validator = GeographyValidator.instance(true);
List<String> multiPolygonsWkt = List.of(
"MULTIPOLYGON Z (((20.0 10.0, 30.0 15.0, 40.0 10.0, 20.0 10.0)), ((0.0 0.0, 10.0 0.0, 10.0 10.0, 0.0 0.0)))",
"MULTIPOLYGON M (((20.0 10.0, 30.0 15.0, 40.0 10.0, 20.0 10.0)), ((0.0 0.0, 10.0 0.0, 10.0 10.0, 0.0 0.0)))"
);
for (String multiPolygon : multiPolygonsWkt) {
IllegalArgumentException ex = expectThrows(
IllegalArgumentException.class,
() -> WellKnownText.fromWKT(validator, true, multiPolygon)
);
assertEquals(ZorMMustIncludeThreeValuesMsg, ex.getMessage());
}
}
@Override
protected MultiPolygon mutateInstance(MultiPolygon instance) {
return null;// TODO implement https://github.com/elastic/elasticsearch/issues/25929

View file

@ -17,8 +17,10 @@ import org.elasticsearch.geometry.utils.WellKnownText;
import java.io.IOException;
import java.text.ParseException;
import java.util.List;
public class PointTests extends BaseGeometryTestCase<Point> {
@Override
protected Point createTestInstance(boolean hasAlt) {
return GeometryTestUtils.randomPoint(hasAlt);
@ -58,6 +60,21 @@ public class PointTests extends BaseGeometryTestCase<Point> {
assertEquals("found Z value [100.0] but [ignore_z_value] parameter is [false]", ex.getMessage());
}
public void testParsePointZorMWithThreeCoordinates() 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)"));
assertEquals(new Point(20, 10, 100), WellKnownText.fromWKT(validator, true, "POINT M (20.0 10.0 100.0)"));
}
public void testParsePointZorMWithTwoCoordinatesThrowsException() {
GeometryValidator validator = GeographyValidator.instance(true);
List<String> pointsWkt = List.of("POINT Z (20.0 10.0)", "POINT M (20.0 10.0)");
for (String point : pointsWkt) {
IllegalArgumentException ex = expectThrows(IllegalArgumentException.class, () -> WellKnownText.fromWKT(validator, true, point));
assertEquals(ZorMMustIncludeThreeValuesMsg, ex.getMessage());
}
}
@Override
protected Point mutateInstance(Point instance) {
return null;// TODO implement https://github.com/elastic/elasticsearch/issues/25929

View file

@ -18,6 +18,7 @@ import org.elasticsearch.geometry.utils.WellKnownText;
import java.io.IOException;
import java.text.ParseException;
import java.util.Collections;
import java.util.List;
public class PolygonTests extends BaseGeometryTestCase<Polygon> {
@Override
@ -134,6 +135,61 @@ public class PolygonTests extends BaseGeometryTestCase<Polygon> {
);
}
public void testParsePolygonZorMWithThreeCoordinates() throws IOException, ParseException {
GeometryValidator validator = GeographyValidator.instance(true);
Polygon expectedZ = new Polygon(
new LinearRing(
new double[] { 20.0, 30.0, 40.0, 20.0 },
new double[] { 10.0, 15.0, 10.0, 10.0 },
new double[] { 100.0, 200.0, 300.0, 100.0 }
)
);
assertEquals(
expectedZ,
WellKnownText.fromWKT(validator, true, "POLYGON Z ((20.0 10.0 100.0, 30.0 15.0 200.0, 40.0 10.0 300.0, 20.0 10.0 100.0))")
);
Polygon expectedM = new Polygon(
new LinearRing(
new double[] { 20.0, 30.0, 40.0, 20.0 },
new double[] { 10.0, 15.0, 10.0, 10.0 },
new double[] { 100.0, 200.0, 300.0, 100.0 }
)
);
assertEquals(
expectedM,
WellKnownText.fromWKT(validator, true, "POLYGON M ((20.0 10.0 100.0, 30.0 15.0 200.0, 40.0 10.0 300.0, 20.0 10.0 100.0))")
);
Polygon expectedZAutoClose = new Polygon(
new LinearRing(
new double[] { 20.0, 30.0, 40.0, 20.0 },
new double[] { 10.0, 15.0, 10.0, 10.0 },
new double[] { 100.0, 200.0, 300.0, 100.0 }
)
);
assertEquals(
expectedZAutoClose,
WellKnownText.fromWKT(validator, true, "POLYGON Z ((20.0 10.0 100.0, 30.0 15.0 200.0, 40.0 10.0 300.0))")
);
}
public void testParsePolygonZorMWithTwoCoordinatesThrowsException() {
GeometryValidator validator = GeographyValidator.instance(true);
List<String> polygonsWkt = List.of(
"POLYGON Z ((20.0 10.0, 30.0 15.0, 40.0 10.0, 20.0 10.0))",
"POLYGON M ((20.0 10.0, 30.0 15.0, 40.0 10.0, 20.0 10.0))"
);
for (String polygon : polygonsWkt) {
IllegalArgumentException ex = expectThrows(
IllegalArgumentException.class,
() -> WellKnownText.fromWKT(validator, true, polygon)
);
assertEquals(ZorMMustIncludeThreeValuesMsg, ex.getMessage());
}
}
@Override
protected Polygon mutateInstance(Polygon instance) {
return null;// TODO implement https://github.com/elastic/elasticsearch/issues/25929