Skip to content

Commit 1dd747e

Browse files
committed
Support explicit Z/M coordinate values in WKT parsing for geometry (#12311)
1 parent cdb6230 commit 1dd747e

File tree

9 files changed

+260
-1
lines changed

9 files changed

+260
-1
lines changed

libs/geo/src/main/java/org/elasticsearch/geometry/utils/WellKnownText.java

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ public class WellKnownText {
4444
public static final String RPAREN = ")";
4545
public static final String COMMA = ",";
4646
public static final String NAN = "NaN";
47+
public static final String Z = "Z";
48+
public static final String M = "M";
4749
public static final int MAX_NESTED_DEPTH = 1000;
4850

4951
private static final String NUMBER = "<NUMBER>";
@@ -440,7 +442,8 @@ public static Geometry fromWKT(GeometryValidator validator, boolean coerce, Stri
440442
*/
441443
private static Geometry parseGeometry(StreamTokenizer stream, boolean coerce, int depth) throws IOException, ParseException {
442444
final String type = nextWord(stream).toLowerCase(Locale.ROOT);
443-
return switch (type) {
445+
final boolean isExplicitlySpecifiesZorM = isZOrMNext(stream);
446+
Geometry geometry = switch (type) {
444447
case "point" -> parsePoint(stream);
445448
case "multipoint" -> parseMultiPoint(stream);
446449
case "linestring" -> parseLine(stream);
@@ -453,6 +456,16 @@ private static Geometry parseGeometry(StreamTokenizer stream, boolean coerce, in
453456
parseCircle(stream);
454457
default -> throw new IllegalArgumentException("Unknown geometry type: " + type);
455458
};
459+
checkZorMAttribute(isExplicitlySpecifiesZorM, geometry.hasZ());
460+
return geometry;
461+
}
462+
463+
private static void checkZorMAttribute(boolean isExplicitlySpecifiesZorM, boolean hasZ) {
464+
if (isExplicitlySpecifiesZorM && hasZ == false) {
465+
throw new IllegalArgumentException(
466+
"When specifying 'Z' or 'M', coordinates must include three values. Only two coordinates were provided"
467+
);
468+
}
456469
}
457470

458471
private static GeometryCollection<Geometry> parseGeometryCollection(StreamTokenizer stream, boolean coerce, int depth)
@@ -710,6 +723,15 @@ private static boolean isNumberNext(StreamTokenizer stream) throws IOException {
710723
return type == StreamTokenizer.TT_WORD;
711724
}
712725

726+
private static boolean isZOrMNext(StreamTokenizer stream) throws ParseException, IOException {
727+
String token = nextWord(stream);
728+
if (token.equals(Z) || token.equals(M)) {
729+
return true;
730+
}
731+
stream.pushBack();
732+
return false;
733+
}
734+
713735
private static String nextEmptyOrOpen(StreamTokenizer stream) throws IOException, ParseException {
714736
final String next = nextWord(stream);
715737
if (next.equals(EMPTY) || next.equals(LPAREN)) {

libs/geo/src/test/java/org/elasticsearch/geometry/BaseGeometryTestCase.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@
2121

2222
abstract class BaseGeometryTestCase<T extends Geometry> extends AbstractWireTestCase<T> {
2323

24+
public static final String ZorMMustIncludeThreeValuesMsg =
25+
"When specifying 'Z' or 'M', coordinates must include three values. Only two coordinates were provided";
26+
2427
@Override
2528
protected final T createTestInstance() {
2629
boolean hasAlt = randomBoolean();

libs/geo/src/test/java/org/elasticsearch/geometry/GeometryCollectionTests.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.text.ParseException;
2020
import java.util.Arrays;
2121
import java.util.Collections;
22+
import java.util.List;
2223

2324
import static org.hamcrest.Matchers.containsString;
2425

@@ -93,6 +94,35 @@ private int countNestedGeometryCollections(GeometryCollection<?> geometry) {
9394
return count;
9495
}
9596

97+
public void testParseGeometryCollectionZorMWithThreeCoordinates() throws IOException, ParseException {
98+
GeometryValidator validator = GeographyValidator.instance(true);
99+
100+
GeometryCollection<Geometry> expected = new GeometryCollection<>(
101+
Arrays.asList(
102+
new Point(20.0, 10.0, 100.0),
103+
new Line(new double[] { 10.0, 20.0 }, new double[] { 5.0, 15.0 }, new double[] { 50.0, 150.0 })
104+
)
105+
);
106+
107+
String point = "(POINT Z (20.0 10.0 100.0)";
108+
String lineString = "LINESTRING M (10.0 5.0 50.0, 20.0 15.0 150.0)";
109+
assertEquals(expected, WellKnownText.fromWKT(validator, true, "GEOMETRYCOLLECTION Z " + point + ", " + lineString + ")"));
110+
111+
assertEquals(expected, WellKnownText.fromWKT(validator, true, "GEOMETRYCOLLECTION M " + point + ", " + lineString + ")"));
112+
}
113+
114+
public void testParseGeometryCollectionZorMWithTwoCoordinatesThrowsException() {
115+
GeometryValidator validator = GeographyValidator.instance(true);
116+
List<String> gcWkt = List.of(
117+
"GEOMETRYCOLLECTION Z (POINT (20.0 10.0), LINESTRING (10.0 5.0, 20.0 15.0))",
118+
"GEOMETRYCOLLECTION M (POINT (20.0 10.0), LINESTRING (10.0 5.0, 20.0 15.0))"
119+
);
120+
for (String gc : gcWkt) {
121+
IllegalArgumentException ex = expectThrows(IllegalArgumentException.class, () -> WellKnownText.fromWKT(validator, true, gc));
122+
assertEquals(ZorMMustIncludeThreeValuesMsg, ex.getMessage());
123+
}
124+
}
125+
96126
@Override
97127
protected GeometryCollection<Geometry> mutateInstance(GeometryCollection<Geometry> instance) {
98128
return null;// TODO implement https://github.com/elastic/elasticsearch/issues/25929

libs/geo/src/test/java/org/elasticsearch/geometry/LineTests.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import java.io.IOException;
1919
import java.text.ParseException;
20+
import java.util.List;
2021

2122
public class LineTests extends BaseGeometryTestCase<Line> {
2223
@Override
@@ -82,6 +83,25 @@ public void testWKTValidation() {
8283
assertEquals("found Z value [6.0] but [ignore_z_value] parameter is [false]", ex.getMessage());
8384
}
8485

86+
public void testParseLineZorMWithThreeCoordinates() throws IOException, ParseException {
87+
GeometryValidator validator = GeographyValidator.instance(true);
88+
89+
Line expectedZ = new Line(new double[] { 20.0, 30.0 }, new double[] { 10.0, 15.0 }, new double[] { 100.0, 200.0 });
90+
assertEquals(expectedZ, WellKnownText.fromWKT(validator, true, "LINESTRING Z (20.0 10.0 100.0, 30.0 15.0 200.0)"));
91+
92+
Line expectedM = new Line(new double[] { 20.0, 30.0 }, new double[] { 10.0, 15.0 }, new double[] { 100.0, 200.0 });
93+
assertEquals(expectedM, WellKnownText.fromWKT(validator, true, "LINESTRING M (20.0 10.0 100.0, 30.0 15.0 200.0)"));
94+
}
95+
96+
public void testParseLineZorMWithTwoCoordinatesThrowsException() {
97+
GeometryValidator validator = GeographyValidator.instance(true);
98+
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)");
99+
for (String line : linesWkt) {
100+
IllegalArgumentException ex = expectThrows(IllegalArgumentException.class, () -> WellKnownText.fromWKT(validator, true, line));
101+
assertEquals(ZorMMustIncludeThreeValuesMsg, ex.getMessage());
102+
}
103+
}
104+
85105
@Override
86106
protected Line mutateInstance(Line instance) {
87107
return null;// TODO implement https://github.com/elastic/elasticsearch/issues/25929

libs/geo/src/test/java/org/elasticsearch/geometry/MultiLineTests.java

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,54 @@ public void testValidation() {
6464
);
6565
}
6666

67+
public void testParseMultiLineZorMWithThreeCoordinates() throws IOException, ParseException {
68+
GeometryValidator validator = GeographyValidator.instance(true);
69+
MultiLine expectedZ = new MultiLine(
70+
List.of(
71+
new Line(new double[] { 20.0, 30.0 }, new double[] { 10.0, 15.0 }, new double[] { 100.0, 200.0 }),
72+
new Line(new double[] { 40.0, 50.0 }, new double[] { 20.0, 25.0 }, new double[] { 300.0, 400.0 })
73+
)
74+
);
75+
assertEquals(
76+
expectedZ,
77+
WellKnownText.fromWKT(
78+
validator,
79+
true,
80+
"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))"
81+
)
82+
);
83+
84+
MultiLine expectedM = new MultiLine(
85+
List.of(
86+
new Line(new double[] { 20.0, 30.0 }, new double[] { 10.0, 15.0 }, new double[] { 100.0, 200.0 }),
87+
new Line(new double[] { 40.0, 50.0 }, new double[] { 20.0, 25.0 }, new double[] { 300.0, 400.0 })
88+
)
89+
);
90+
assertEquals(
91+
expectedM,
92+
WellKnownText.fromWKT(
93+
validator,
94+
true,
95+
"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))"
96+
)
97+
);
98+
}
99+
100+
public void testParseMultiLineZorMWithTwoCoordinatesThrowsException() {
101+
GeometryValidator validator = GeographyValidator.instance(true);
102+
List<String> multiLinesWkt = List.of(
103+
"MULTILINESTRING Z ((20.0 10.0, 30.0 15.0), (40.0 20.0, 50.0 25.0))",
104+
"MULTILINESTRING M ((20.0 10.0, 30.0 15.0), (40.0 20.0, 50.0 25.0))"
105+
);
106+
for (String multiLine : multiLinesWkt) {
107+
IllegalArgumentException ex = expectThrows(
108+
IllegalArgumentException.class,
109+
() -> WellKnownText.fromWKT(validator, true, multiLine)
110+
);
111+
assertEquals(ZorMMustIncludeThreeValuesMsg, ex.getMessage());
112+
}
113+
}
114+
67115
@Override
68116
protected MultiLine mutateInstance(MultiLine instance) {
69117
return null;// TODO implement https://github.com/elastic/elasticsearch/issues/25929

libs/geo/src/test/java/org/elasticsearch/geometry/MultiPointTests.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,27 @@ public void testValidation() {
7171
StandardValidator.instance(true).validate(new MultiPoint(Collections.singletonList(new Point(2, 1, 3))));
7272
}
7373

74+
public void testParseMultiPointWithThreeCoordinates() throws IOException, ParseException {
75+
GeometryValidator validator = GeographyValidator.instance(true);
76+
MultiPoint expectedZ = new MultiPoint(Arrays.asList(new Point(10, 20, 30), new Point(40, 50, 60)));
77+
MultiPoint expectedM = new MultiPoint(Arrays.asList(new Point(10, 20, 30), new Point(40, 50, 60)));
78+
79+
assertEquals(expectedZ, WellKnownText.fromWKT(validator, true, "MULTIPOINT Z (10 20 30, 40 50 60)"));
80+
assertEquals(expectedM, WellKnownText.fromWKT(validator, true, "MULTIPOINT M (10 20 30, 40 50 60)"));
81+
}
82+
83+
public void testParseMultiPointWithTwoCoordinatesThrowsException() {
84+
GeometryValidator validator = GeographyValidator.instance(true);
85+
List<String> multiPointsWkt = List.of("MULTIPOINT Z (10 20, 40 50)", "MULTIPOINT M (10 20, 40 50)");
86+
for (String multiPoint : multiPointsWkt) {
87+
IllegalArgumentException ex = expectThrows(
88+
IllegalArgumentException.class,
89+
() -> WellKnownText.fromWKT(validator, true, multiPoint)
90+
);
91+
assertEquals(ZorMMustIncludeThreeValuesMsg, ex.getMessage());
92+
}
93+
}
94+
7495
@Override
7596
protected MultiPoint mutateInstance(MultiPoint instance) {
7697
return null;// TODO implement https://github.com/elastic/elasticsearch/issues/25929

libs/geo/src/test/java/org/elasticsearch/geometry/MultiPolygonTests.java

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,48 @@ public void testValidation() {
8080
);
8181
}
8282

83+
public void testParseMultiPolygonZorMWithThreeCoordinates() throws IOException, ParseException {
84+
GeometryValidator validator = GeographyValidator.instance(true);
85+
86+
MultiPolygon expected = new MultiPolygon(
87+
List.of(
88+
new Polygon(
89+
new LinearRing(
90+
new double[] { 20.0, 30.0, 40.0, 20.0 },
91+
new double[] { 10.0, 15.0, 10.0, 10.0 },
92+
new double[] { 100.0, 200.0, 300.0, 100.0 }
93+
)
94+
),
95+
new Polygon(
96+
new LinearRing(
97+
new double[] { 0.0, 10.0, 10.0, 0.0 },
98+
new double[] { 0.0, 0.0, 10.0, 0.0 },
99+
new double[] { 10.0, 20.0, 30.0, 10.0 }
100+
)
101+
)
102+
)
103+
);
104+
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)";
105+
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)";
106+
assertEquals(expected, WellKnownText.fromWKT(validator, true, "MULTIPOLYGON Z ((" + polygonA + "), (" + polygonB + "))"));
107+
assertEquals(expected, WellKnownText.fromWKT(validator, true, "MULTIPOLYGON M ((" + polygonA + "), (" + polygonB + "))"));
108+
}
109+
110+
public void testParseMultiPolygonZorMWithTwoCoordinatesThrowsException() {
111+
GeometryValidator validator = GeographyValidator.instance(true);
112+
List<String> multiPolygonsWkt = List.of(
113+
"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)))",
114+
"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)))"
115+
);
116+
for (String multiPolygon : multiPolygonsWkt) {
117+
IllegalArgumentException ex = expectThrows(
118+
IllegalArgumentException.class,
119+
() -> WellKnownText.fromWKT(validator, true, multiPolygon)
120+
);
121+
assertEquals(ZorMMustIncludeThreeValuesMsg, ex.getMessage());
122+
}
123+
}
124+
83125
@Override
84126
protected MultiPolygon mutateInstance(MultiPolygon instance) {
85127
return null;// TODO implement https://github.com/elastic/elasticsearch/issues/25929

libs/geo/src/test/java/org/elasticsearch/geometry/PointTests.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@
1717

1818
import java.io.IOException;
1919
import java.text.ParseException;
20+
import java.util.List;
2021

2122
public class PointTests extends BaseGeometryTestCase<Point> {
23+
2224
@Override
2325
protected Point createTestInstance(boolean hasAlt) {
2426
return GeometryTestUtils.randomPoint(hasAlt);
@@ -58,6 +60,21 @@ public void testWKTValidation() {
5860
assertEquals("found Z value [100.0] but [ignore_z_value] parameter is [false]", ex.getMessage());
5961
}
6062

63+
public void testParsePointZorMWithThreeCoordinates() throws IOException, ParseException {
64+
GeometryValidator validator = GeographyValidator.instance(true);
65+
assertEquals(new Point(20, 10, 100), WellKnownText.fromWKT(validator, true, "POINT Z (20.0 10.0 100.0)"));
66+
assertEquals(new Point(20, 10, 100), WellKnownText.fromWKT(validator, true, "POINT M (20.0 10.0 100.0)"));
67+
}
68+
69+
public void testParsePointZorMWithTwoCoordinatesThrowsException() {
70+
GeometryValidator validator = GeographyValidator.instance(true);
71+
List<String> pointsWkt = List.of("POINT Z (20.0 10.0)", "POINT M (20.0 10.0)");
72+
for (String point : pointsWkt) {
73+
IllegalArgumentException ex = expectThrows(IllegalArgumentException.class, () -> WellKnownText.fromWKT(validator, true, point));
74+
assertEquals(ZorMMustIncludeThreeValuesMsg, ex.getMessage());
75+
}
76+
}
77+
6178
@Override
6279
protected Point mutateInstance(Point instance) {
6380
return null;// TODO implement https://github.com/elastic/elasticsearch/issues/25929

libs/geo/src/test/java/org/elasticsearch/geometry/PolygonTests.java

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import java.io.IOException;
1919
import java.text.ParseException;
2020
import java.util.Collections;
21+
import java.util.List;
2122

2223
public class PolygonTests extends BaseGeometryTestCase<Polygon> {
2324
@Override
@@ -134,6 +135,61 @@ public void testWKTValidation() {
134135
);
135136
}
136137

138+
public void testParsePolygonZorMWithThreeCoordinates() throws IOException, ParseException {
139+
GeometryValidator validator = GeographyValidator.instance(true);
140+
141+
Polygon expectedZ = new Polygon(
142+
new LinearRing(
143+
new double[] { 20.0, 30.0, 40.0, 20.0 },
144+
new double[] { 10.0, 15.0, 10.0, 10.0 },
145+
new double[] { 100.0, 200.0, 300.0, 100.0 }
146+
)
147+
);
148+
assertEquals(
149+
expectedZ,
150+
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))")
151+
);
152+
153+
Polygon expectedM = new Polygon(
154+
new LinearRing(
155+
new double[] { 20.0, 30.0, 40.0, 20.0 },
156+
new double[] { 10.0, 15.0, 10.0, 10.0 },
157+
new double[] { 100.0, 200.0, 300.0, 100.0 }
158+
)
159+
);
160+
assertEquals(
161+
expectedM,
162+
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))")
163+
);
164+
165+
Polygon expectedZAutoClose = new Polygon(
166+
new LinearRing(
167+
new double[] { 20.0, 30.0, 40.0, 20.0 },
168+
new double[] { 10.0, 15.0, 10.0, 10.0 },
169+
new double[] { 100.0, 200.0, 300.0, 100.0 }
170+
)
171+
);
172+
assertEquals(
173+
expectedZAutoClose,
174+
WellKnownText.fromWKT(validator, true, "POLYGON Z ((20.0 10.0 100.0, 30.0 15.0 200.0, 40.0 10.0 300.0))")
175+
);
176+
}
177+
178+
public void testParsePolygonZorMWithTwoCoordinatesThrowsException() {
179+
GeometryValidator validator = GeographyValidator.instance(true);
180+
List<String> polygonsWkt = List.of(
181+
"POLYGON Z ((20.0 10.0, 30.0 15.0, 40.0 10.0, 20.0 10.0))",
182+
"POLYGON M ((20.0 10.0, 30.0 15.0, 40.0 10.0, 20.0 10.0))"
183+
);
184+
for (String polygon : polygonsWkt) {
185+
IllegalArgumentException ex = expectThrows(
186+
IllegalArgumentException.class,
187+
() -> WellKnownText.fromWKT(validator, true, polygon)
188+
);
189+
assertEquals(ZorMMustIncludeThreeValuesMsg, ex.getMessage());
190+
}
191+
}
192+
137193
@Override
138194
protected Polygon mutateInstance(Polygon instance) {
139195
return null;// TODO implement https://github.com/elastic/elasticsearch/issues/25929

0 commit comments

Comments
 (0)