Skip to content

Commit 63c97b0

Browse files
authored
Add geojson support for XYPoint (#162)
* Add geojson support for XYPoint Signed-off-by: Heemin Kim <heemin@amazon.com>
1 parent 9b4e9a6 commit 63c97b0

File tree

5 files changed

+464
-86
lines changed

5 files changed

+464
-86
lines changed

src/main/java/org/opensearch/geospatial/index/mapper/xypoint/XYPointParser.java

Lines changed: 194 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
import java.io.IOException;
99
import java.util.Collections;
10+
import java.util.HashMap;
11+
import java.util.Map;
1012
import java.util.Objects;
1113

1214
import org.opensearch.OpenSearchParseException;
@@ -15,13 +17,18 @@
1517
import org.opensearch.common.xcontent.XContentParser;
1618
import org.opensearch.common.xcontent.XContentSubParser;
1719
import org.opensearch.common.xcontent.support.MapXContentParser;
20+
import org.opensearch.geometry.ShapeType;
1821

1922
/**
2023
* Parse the value and set XYPoint represented as a String, Object, WKT, array.
2124
*/
2225
public class XYPointParser {
26+
private static final String ERR_MSG_INVALID_TOKEN = "token [{}] not allowed";
27+
private static final String ERR_MSG_INVALID_FIELDS = "field must be either [x|y], or [type|coordinates]";
2328
private static final String X_PARAMETER = "x";
2429
private static final String Y_PARAMETER = "y";
30+
public static final String GEOJSON_TYPE = "type";
31+
public static final String GEOJSON_COORDS = "coordinates";
2532
private static final String NULL_VALUE_PARAMETER = "null_value";
2633
private static final Boolean TRUE = true;
2734

@@ -33,7 +40,7 @@ public class XYPointParser {
3340
* @return {@link XYPoint} after setting the x and y coordinates parsed from the parse
3441
* @throws OpenSearchParseException
3542
*/
36-
public static XYPoint parseXYPoint(Object value, final boolean ignoreZValue) throws OpenSearchParseException {
43+
public static XYPoint parseXYPoint(final Object value, final boolean ignoreZValue) throws OpenSearchParseException {
3744
Objects.requireNonNull(value, "input value which needs to be parsed should not be null");
3845

3946
try (
@@ -54,13 +61,13 @@ public static XYPoint parseXYPoint(Object value, final boolean ignoreZValue) thr
5461
}
5562

5663
/**
57-
* Parse the values to set the XYPoint which was represented as a String, Object, WKT or an array.
58-
*
64+
* Parse the values to set the XYPoint which was represented as a String, Object, WKT, Array, or GeoJson.
5965
* <ul>
60-
* <li> String: "100.35, -200.54" </li>
61-
* <li> Object: {"x" : 100.35, "y" : -200.54} </li>
62-
* <li> WKT: "POINT (-200.54 100.35)"</li>
63-
* <li> Array: [ -200.54, 100.35 ]</li>
66+
* <li>Object: <pre>{@code {"x": <x>, "y": <y}}</pre></li>
67+
* <li>String: <pre>{@code "<x>,<y>"}</pre></li>
68+
* <li>WKT: <pre>{@code "POINT (<x> <y>)"}</pre></li>
69+
* <li>Array: <pre>{@code [<x>, <y>]}</pre></li>
70+
* <li>GeoJson: <pre>{@code {"type": "Point", "coordinates": [<x>, <y>]}}</pre><li>
6471
* </ul>
6572
*
6673
* @param parser {@link XContentParser} to parse the value from
@@ -69,103 +76,207 @@ public static XYPoint parseXYPoint(Object value, final boolean ignoreZValue) thr
6976
* @throws IOException
7077
* @throws OpenSearchParseException
7178
*/
72-
public static XYPoint parseXYPoint(XContentParser parser, final boolean ignoreZValue) throws IOException, OpenSearchParseException {
79+
public static XYPoint parseXYPoint(final XContentParser parser, final boolean ignoreZValue) throws IOException,
80+
OpenSearchParseException {
7381
Objects.requireNonNull(parser, "parser should not be null");
74-
7582
XYPoint point = new XYPoint();
76-
double x = Double.NaN;
77-
double y = Double.NaN;
78-
79-
if (parser.currentToken() == XContentParser.Token.START_OBJECT) {
80-
try (XContentSubParser subParser = new XContentSubParser(parser)) {
81-
while (subParser.nextToken() != XContentParser.Token.END_OBJECT) {
82-
if (subParser.currentToken() != XContentParser.Token.FIELD_NAME) {
83-
throw new OpenSearchParseException("token [{}] not allowed", subParser.currentToken());
84-
}
85-
String field = subParser.currentName();
86-
if (!(X_PARAMETER.equals(field) || Y_PARAMETER.equals(field))) {
87-
throw new OpenSearchParseException("field must be either [{}] or [{}]", X_PARAMETER, Y_PARAMETER);
88-
}
89-
if (X_PARAMETER.equals(field)) {
90-
subParser.nextToken();
91-
switch (subParser.currentToken()) {
92-
case VALUE_NUMBER:
93-
case VALUE_STRING:
94-
try {
95-
x = subParser.doubleValue(TRUE);
96-
} catch (NumberFormatException numberFormatException) {
97-
throw new OpenSearchParseException("[x] must be valid double value", numberFormatException);
98-
}
99-
break;
100-
default:
101-
throw new OpenSearchParseException("[x] must be a number");
102-
}
103-
}
104-
if (Y_PARAMETER.equals(field)) {
105-
subParser.nextToken();
106-
switch (subParser.currentToken()) {
107-
case VALUE_NUMBER:
108-
case VALUE_STRING:
109-
try {
110-
y = subParser.doubleValue(TRUE);
111-
} catch (NumberFormatException numberFormatException) {
112-
throw new OpenSearchParseException("[y] must be valid double value", numberFormatException);
113-
}
114-
break;
115-
default:
116-
throw new OpenSearchParseException("[y] must be a number");
117-
}
118-
}
119-
}
83+
switch (parser.currentToken()) {
84+
case START_OBJECT:
85+
parseXYPointObject(parser, point, ignoreZValue);
86+
break;
87+
case START_ARRAY:
88+
parseXYPointArray(parser, point, ignoreZValue);
89+
break;
90+
case VALUE_STRING:
91+
String val = parser.text();
92+
point.resetFromString(val, ignoreZValue);
93+
break;
94+
default:
95+
throw new OpenSearchParseException("expecting xy_point as an array, a string, or an object format");
96+
}
97+
return point;
98+
}
99+
100+
/**
101+
* Parse point in either basic object format or GeoJson format
102+
*
103+
* Parser is expected to be pointing the start of the object.
104+
* ex) Parser is pointing { in {"x": 12.3, "y": 45.6}
105+
*
106+
* @param parser {@link XContentParser} to parse the value from
107+
* @param point {@link XYPoint} to be returned after setting the x and y coordinates parsed from the parse
108+
* @return {@link XYPoint} after setting the x and y coordinates parsed from the parse
109+
* @throws IOException
110+
*/
111+
private static XYPoint parseXYPointObject(final XContentParser parser, final XYPoint point, final boolean ignoreZValue)
112+
throws IOException {
113+
try (XContentSubParser subParser = new XContentSubParser(parser)) {
114+
if (subParser.nextToken() != XContentParser.Token.FIELD_NAME) {
115+
throw new OpenSearchParseException(ERR_MSG_INVALID_TOKEN, subParser.currentToken());
120116
}
121-
if (Double.isNaN(x)) {
122-
throw new OpenSearchParseException("field [{}] missing", X_PARAMETER);
117+
118+
String field = subParser.currentName();
119+
if (X_PARAMETER.equals(field) || Y_PARAMETER.equals(field)) {
120+
parseXYPointObjectBasicFields(subParser, point);
121+
} else if (GEOJSON_TYPE.equals(field) || GEOJSON_COORDS.equals(field)) {
122+
parseGeoJsonFields(subParser, point, ignoreZValue);
123+
} else {
124+
throw new OpenSearchParseException(ERR_MSG_INVALID_FIELDS);
123125
}
124-
if (Double.isNaN(y)) {
125-
throw new OpenSearchParseException("field [{}] missing", Y_PARAMETER);
126+
127+
if (subParser.nextToken() != XContentParser.Token.END_OBJECT) {
128+
throw new OpenSearchParseException(ERR_MSG_INVALID_FIELDS);
126129
}
127-
return point.reset(x, y);
130+
131+
return point;
128132
}
133+
}
129134

130-
if (parser.currentToken() == XContentParser.Token.START_ARRAY) {
131-
return parseXYPointArray(parser, ignoreZValue, x, y);
135+
/**
136+
* Parse point in basic object format
137+
*
138+
* Parser is expected to be pointing the first field of the object.
139+
* ex) Parser is pointing x in {"x": 12.3, "y": 45.6}
140+
*
141+
* @param parser {@link XContentParser} to parse the value from
142+
* @param point {@link XYPoint} to be returned after setting the x and y coordinates parsed from the parse
143+
* @return {@link XYPoint} after setting the x and y coordinates parsed from the parse
144+
* @throws IOException
145+
*/
146+
private static XYPoint parseXYPointObjectBasicFields(final XContentParser parser, final XYPoint point) throws IOException {
147+
final int numberOfFields = 2;
148+
Map<String, Double> data = new HashMap<>();
149+
for (int i = 0; i < numberOfFields; i++) {
150+
if (i != 0) {
151+
parser.nextToken();
152+
}
153+
154+
if (parser.currentToken() != XContentParser.Token.FIELD_NAME) {
155+
break;
156+
}
157+
158+
String field = parser.currentName();
159+
if (X_PARAMETER.equals(field) == false && Y_PARAMETER.equals(field) == false) {
160+
throw new OpenSearchParseException(ERR_MSG_INVALID_FIELDS);
161+
}
162+
switch (parser.nextToken()) {
163+
case VALUE_NUMBER:
164+
case VALUE_STRING:
165+
try {
166+
data.put(field, parser.doubleValue(true));
167+
} catch (NumberFormatException e) {
168+
throw new OpenSearchParseException("[{}] and [{}] must be valid double values", e, X_PARAMETER, Y_PARAMETER);
169+
}
170+
break;
171+
default:
172+
throw new OpenSearchParseException("{} must be a number", field);
173+
}
132174
}
133175

134-
if (parser.currentToken() == XContentParser.Token.VALUE_STRING) {
135-
String val = parser.text();
136-
return point.resetFromString(val, ignoreZValue);
176+
if (data.get(X_PARAMETER) == null) {
177+
throw new OpenSearchParseException("field [{}] missing", X_PARAMETER);
178+
}
179+
if (data.get(Y_PARAMETER) == null) {
180+
throw new OpenSearchParseException("field [{}] missing", Y_PARAMETER);
137181
}
138-
throw new OpenSearchParseException("Expected xy_point. But, the provided mapping is not of type xy_point");
182+
183+
return point.reset(data.get(X_PARAMETER), data.get(Y_PARAMETER));
139184
}
140185

141186
/**
142-
* Parse the values to set the XYPoint which was represented as an array.
187+
* Parse point in GeoJson format
188+
*
189+
* Parser is expected to be pointing the first field of the object.
190+
* ex) Parser is pointing type in {"type": "Point", "coordinates": [12.3, 45.6]}
143191
*
144-
* @param subParser {@link XContentParser} to parse the values from an array
192+
* @param parser {@link XContentParser} to parse the value from
193+
* @param point {@link XYPoint} to be returned after setting the x and y coordinates parsed from the parse
145194
* @param ignoreZValue boolean parameter which decides if third coordinate needs to be ignored or not
146-
* @param x x coordinate that will be set by parsing the value from array
147-
* @param y y coordinate that will be set by parsing the value from array
148195
* @return {@link XYPoint} after setting the x and y coordinates parsed from the parse
149196
* @throws IOException
150197
*/
151-
private static XYPoint parseXYPointArray(XContentParser subParser, final boolean ignoreZValue, double x, double y) throws IOException {
152-
XYPoint point = new XYPoint();
153-
int element = 0;
154-
while (subParser.nextToken() != XContentParser.Token.END_ARRAY) {
155-
if (subParser.currentToken() != XContentParser.Token.VALUE_NUMBER) {
156-
throw new OpenSearchParseException("numeric value expected");
198+
private static XYPoint parseGeoJsonFields(final XContentParser parser, final XYPoint point, final boolean ignoreZValue)
199+
throws IOException {
200+
final int numberOfFields = 2;
201+
boolean hasTypePoint = false;
202+
boolean hasCoordinates = false;
203+
for (int i = 0; i < numberOfFields; i++) {
204+
if (i != 0) {
205+
parser.nextToken();
206+
}
207+
208+
if (parser.currentToken() != XContentParser.Token.FIELD_NAME) {
209+
if (hasTypePoint == false) {
210+
throw new OpenSearchParseException("field [{}] missing", GEOJSON_TYPE);
211+
}
212+
if (hasCoordinates == false) {
213+
throw new OpenSearchParseException("field [{}] missing", GEOJSON_COORDS);
214+
}
157215
}
158-
element++;
159-
if (element == 1) {
160-
x = subParser.doubleValue();
161-
} else if (element == 2) {
162-
y = subParser.doubleValue();
163-
} else if (element == 3) {
164-
XYPoint.assertZValue(ignoreZValue, subParser.doubleValue());
216+
217+
if (GEOJSON_TYPE.equals(parser.currentName())) {
218+
if (parser.nextToken() != XContentParser.Token.VALUE_STRING) {
219+
throw new OpenSearchParseException("{} must be a string", GEOJSON_TYPE);
220+
}
221+
222+
// To be consistent with geo_shape parsing, ignore case here as well.
223+
if (ShapeType.POINT.name().equalsIgnoreCase(parser.text()) == false) {
224+
throw new OpenSearchParseException("{} must be Point", GEOJSON_TYPE);
225+
}
226+
hasTypePoint = true;
227+
} else if (GEOJSON_COORDS.equals(parser.currentName())) {
228+
if (parser.nextToken() != XContentParser.Token.START_ARRAY) {
229+
throw new OpenSearchParseException("{} must be an array", GEOJSON_COORDS);
230+
}
231+
parseXYPointArray(parser, point, ignoreZValue);
232+
hasCoordinates = true;
165233
} else {
166-
throw new OpenSearchParseException("[xy_point] field type does not accept more than 3 dimensions");
234+
throw new OpenSearchParseException(ERR_MSG_INVALID_FIELDS);
167235
}
168236
}
169-
return point.reset(x, y);
237+
238+
return point;
239+
}
240+
241+
/**
242+
* Parse point in an array format
243+
*
244+
* Parser is expected to be pointing the start of the array.
245+
* ex) Parser is pointing [ in [12.3, 45.6]
246+
*
247+
* @param parser {@link XContentParser} to parse the value from
248+
* @param point {@link XYPoint} to be returned after setting the x and y coordinates parsed from the parse
249+
* @param ignoreZValue boolean parameter which decides if third coordinate needs to be ignored or not
250+
* @return {@link XYPoint} after setting the x and y coordinates parsed from the parse
251+
* @throws IOException
252+
*/
253+
private static XYPoint parseXYPointArray(final XContentParser parser, final XYPoint point, final boolean ignoreZValue)
254+
throws IOException {
255+
try (XContentSubParser subParser = new XContentSubParser(parser)) {
256+
double x = Double.NaN;
257+
double y = Double.NaN;
258+
259+
int element = 0;
260+
while (subParser.nextToken() != XContentParser.Token.END_ARRAY) {
261+
if (parser.currentToken() != XContentParser.Token.VALUE_NUMBER) {
262+
throw new OpenSearchParseException("numeric value expected");
263+
}
264+
element++;
265+
if (element == 1) {
266+
x = parser.doubleValue();
267+
} else if (element == 2) {
268+
y = parser.doubleValue();
269+
} else if (element == 3) {
270+
XYPoint.assertZValue(ignoreZValue, parser.doubleValue());
271+
} else {
272+
throw new OpenSearchParseException("[xy_point] field type does not accept more than 3 values");
273+
}
274+
}
275+
276+
if (element < 2) {
277+
throw new OpenSearchParseException("[xy_point] field type should have at least two dimensions");
278+
}
279+
return point.reset(x, y);
280+
}
170281
}
171282
}

src/test/java/org/opensearch/geospatial/index/mapper/xypoint/XYPointFieldMapperIT.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919
public class XYPointFieldMapperIT extends GeospatialRestTestCase {
2020
private static final String FIELD_X_KEY = "x";
2121
private static final String FIELD_Y_KEY = "y";
22+
private static final String FIELD_GEOJSON_TYPE_KEY = "type";
23+
private static final String FIELD_GEOJSON_TYPE_VALUE = "Point";
24+
private static final String FIELD_GEOJSON_COORDINATES_KEY = "coordinates";
2225

2326
public void testMappingWithXYPointField() throws Exception {
2427
String indexName = GeospatialTestHelper.randomLowerCaseString();
@@ -85,6 +88,20 @@ public void testIndexWithXYPointFieldAsObjectFormat() throws Exception {
8588
deleteIndex(indexName);
8689
}
8790

91+
public void testIndexWithXYPointFieldAsGeoJsonFormat() throws Exception {
92+
String indexName = GeospatialTestHelper.randomLowerCaseString();
93+
String fieldName = GeospatialTestHelper.randomLowerCaseString();
94+
createIndex(indexName, Settings.EMPTY, Map.of(fieldName, XYPointFieldMapper.CONTENT_TYPE));
95+
final Point point = ShapeObjectBuilder.randomPoint(randomBoolean());
96+
String docID = indexDocument(indexName, getDocumentWithObjectValueForXYPoint(fieldName, point));
97+
assertTrue("failed to index document", getIndexDocumentCount(indexName) > 0);
98+
final Map<String, Object> document = getDocument(docID, indexName);
99+
assertNotNull("failed to get indexed document", document);
100+
String expectedValue = String.format(Locale.ROOT, "{x=%s, y=%s}", point.getX(), point.getY());
101+
assertEquals("failed to index xy_point", expectedValue, document.get(fieldName).toString());
102+
deleteIndex(indexName);
103+
}
104+
88105
private String getDocumentWithWKTValueForXYPoint(String fieldName, Geometry geometry) throws Exception {
89106
return buildContentAsString(build -> build.field(fieldName, geometry.toString()));
90107
}
@@ -106,4 +123,13 @@ private String getDocumentWithObjectValueForXYPoint(String fieldName, Point poin
106123
});
107124
}
108125

126+
private String getDocumentWithGeoJsonValueForXYPoint(String fieldName, Point point) throws Exception {
127+
return buildContentAsString(build -> {
128+
build.startObject(fieldName);
129+
build.field(FIELD_GEOJSON_TYPE_KEY, FIELD_GEOJSON_TYPE_VALUE);
130+
build.array(FIELD_GEOJSON_COORDINATES_KEY, new double[] { point.getX(), point.getY() });
131+
build.endObject();
132+
});
133+
}
134+
109135
}

0 commit comments

Comments
 (0)