Skip to content

Commit

Permalink
[GEO] Add WKT Support to GeoBoundingBoxQueryBuilder
Browse files Browse the repository at this point in the history
Add WKT BBOX parsing support to GeoBoundingBoxQueryBuilder.
  • Loading branch information
nknize committed Jan 15, 2018
1 parent 18463e7 commit 5ed25f1
Show file tree
Hide file tree
Showing 5 changed files with 181 additions and 56 deletions.
25 changes: 25 additions & 0 deletions docs/reference/query-dsl/geo-bounding-box-query.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,31 @@ GET /_search
--------------------------------------------------
// CONSOLE

[float]
===== Bounding Box as Well-Known Text (WKT)

[source,js]
--------------------------------------------------
GET /_search
{
"query": {
"bool" : {
"must" : {
"match_all" : {}
},
"filter" : {
"geo_bounding_box" : {
"pin.location" : {
"wkt" : "BBOX (-74.1, -71.12, 40.73, 40.01)"
}
}
}
}
}
}
--------------------------------------------------
// CONSOLE

[float]
===== Geohash

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ private GeoWKTParser() {}

public static ShapeBuilder parse(XContentParser parser)
throws IOException, ElasticsearchParseException {
return parseExpectedType(parser, null);
}

/** throws an exception if the parsed geometry type does not match the expected shape type */
public static ShapeBuilder parseExpectedType(XContentParser parser, final GeoShapeType shapeType)
throws IOException, ElasticsearchParseException {
FastStringReader reader = new FastStringReader(parser.text());
try {
// setup the tokenizer; configured to read words w/o numbers
Expand All @@ -77,7 +83,7 @@ public static ShapeBuilder parse(XContentParser parser)
tokenizer.wordChars('.', '.');
tokenizer.whitespaceChars(0, ' ');
tokenizer.commentChar('#');
ShapeBuilder builder = parseGeometry(tokenizer);
ShapeBuilder builder = parseGeometry(tokenizer, shapeType);
checkEOF(tokenizer);
return builder;
} finally {
Expand All @@ -86,8 +92,14 @@ public static ShapeBuilder parse(XContentParser parser)
}

/** parse geometry from the stream tokenizer */
private static ShapeBuilder parseGeometry(StreamTokenizer stream) throws IOException, ElasticsearchParseException {
private static ShapeBuilder parseGeometry(StreamTokenizer stream, GeoShapeType shapeType)
throws IOException, ElasticsearchParseException {
final GeoShapeType type = GeoShapeType.forName(nextWord(stream));
if (shapeType != null && shapeType != GeoShapeType.GEOMETRYCOLLECTION) {
if (type.wktName().equals(shapeType.wktName()) == false) {
throw new ElasticsearchParseException("Expected geometry type [{}] but found [{}]", shapeType, type);
}
}
switch (type) {
case POINT:
return parsePoint(stream);
Expand Down Expand Up @@ -228,9 +240,10 @@ private static GeometryCollectionBuilder parseGeometryCollection(StreamTokenizer
if (nextEmptyOrOpen(stream).equals(EMPTY)) {
return null;
}
GeometryCollectionBuilder builder = new GeometryCollectionBuilder().shape(parseGeometry(stream));
GeometryCollectionBuilder builder = new GeometryCollectionBuilder().shape(
parseGeometry(stream, GeoShapeType.GEOMETRYCOLLECTION));
while (nextCloserOrComma(stream).equals(COMMA)) {
builder.shape(parseGeometry(stream));
builder.shape(parseGeometry(stream, null));
}
return builder;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@
import org.elasticsearch.common.ParsingException;
import org.elasticsearch.common.geo.GeoHashUtils;
import org.elasticsearch.common.geo.GeoPoint;
import org.elasticsearch.common.geo.GeoShapeType;
import org.elasticsearch.common.geo.GeoUtils;
import org.elasticsearch.common.geo.builders.EnvelopeBuilder;
import org.elasticsearch.common.geo.parsers.GeoWKTParser;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.xcontent.XContentBuilder;
Expand Down Expand Up @@ -62,7 +65,6 @@ public class GeoBoundingBoxQueryBuilder extends AbstractQueryBuilder<GeoBounding

private static final ParseField TYPE_FIELD = new ParseField("type");
private static final ParseField VALIDATION_METHOD_FIELD = new ParseField("validation_method");
private static final ParseField FIELD_FIELD = new ParseField("field");
private static final ParseField TOP_FIELD = new ParseField("top");
private static final ParseField BOTTOM_FIELD = new ParseField("bottom");
private static final ParseField LEFT_FIELD = new ParseField("left");
Expand All @@ -72,6 +74,8 @@ public class GeoBoundingBoxQueryBuilder extends AbstractQueryBuilder<GeoBounding
private static final ParseField TOP_RIGHT_FIELD = new ParseField("top_right");
private static final ParseField BOTTOM_LEFT_FIELD = new ParseField("bottom_left");
private static final ParseField IGNORE_UNMAPPED_FIELD = new ParseField("ignore_unmapped");
private static final ParseField WKT_FIELD = new ParseField("wkt");


/** Name of field holding geo coordinates to compute the bounding box on.*/
private final String fieldName;
Expand Down Expand Up @@ -378,68 +382,25 @@ protected void doXContent(XContentBuilder builder, Params params) throws IOExcep
public static GeoBoundingBoxQueryBuilder fromXContent(XContentParser parser) throws IOException {
String fieldName = null;

double top = Double.NaN;
double bottom = Double.NaN;
double left = Double.NaN;
double right = Double.NaN;

float boost = AbstractQueryBuilder.DEFAULT_BOOST;
String queryName = null;
String currentFieldName = null;
XContentParser.Token token;
GeoValidationMethod validationMethod = null;
boolean ignoreUnmapped = DEFAULT_IGNORE_UNMAPPED;

GeoPoint sparse = new GeoPoint();

Rectangle bbox = null;
String type = "memory";

while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
if (token == XContentParser.Token.FIELD_NAME) {
currentFieldName = parser.currentName();
} else if (token == XContentParser.Token.START_OBJECT) {
fieldName = currentFieldName;

while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
if (token == XContentParser.Token.FIELD_NAME) {
currentFieldName = parser.currentName();
token = parser.nextToken();
if (FIELD_FIELD.match(currentFieldName)) {
fieldName = parser.text();
} else if (TOP_FIELD.match(currentFieldName)) {
top = parser.doubleValue();
} else if (BOTTOM_FIELD.match(currentFieldName)) {
bottom = parser.doubleValue();
} else if (LEFT_FIELD.match(currentFieldName)) {
left = parser.doubleValue();
} else if (RIGHT_FIELD.match(currentFieldName)) {
right = parser.doubleValue();
} else {
if (TOP_LEFT_FIELD.match(currentFieldName)) {
GeoUtils.parseGeoPoint(parser, sparse);
top = sparse.getLat();
left = sparse.getLon();
} else if (BOTTOM_RIGHT_FIELD.match(currentFieldName)) {
GeoUtils.parseGeoPoint(parser, sparse);
bottom = sparse.getLat();
right = sparse.getLon();
} else if (TOP_RIGHT_FIELD.match(currentFieldName)) {
GeoUtils.parseGeoPoint(parser, sparse);
top = sparse.getLat();
right = sparse.getLon();
} else if (BOTTOM_LEFT_FIELD.match(currentFieldName)) {
GeoUtils.parseGeoPoint(parser, sparse);
bottom = sparse.getLat();
left = sparse.getLon();
} else {
throw new ElasticsearchParseException("failed to parse [{}] query. unexpected field [{}]",
NAME, currentFieldName);
}
}
} else {
throw new ElasticsearchParseException("failed to parse [{}] query. field name expected but [{}] found",
NAME, token);
}
try {
bbox = parseBoundingBox(parser);
fieldName = currentFieldName;
} catch (Exception e) {
throw new ElasticsearchParseException("failed to parse [{}] query. [{}]", NAME, e.getMessage());
}
} else if (token.isValue()) {
if (AbstractQueryBuilder.NAME_FIELD.match(currentFieldName)) {
Expand All @@ -459,8 +420,13 @@ public static GeoBoundingBoxQueryBuilder fromXContent(XContentParser parser) thr
}
}

final GeoPoint topLeft = sparse.reset(top, left); //just keep the object
final GeoPoint bottomRight = new GeoPoint(bottom, right);
if (bbox == null) {
throw new ElasticsearchParseException("failed to parse [{}] query. bounding box not provided", NAME);
}

final GeoPoint topLeft = new GeoPoint(bbox.maxLat, bbox.minLon); //just keep the object
final GeoPoint bottomRight = new GeoPoint(bbox.minLat, bbox.maxLon);

GeoBoundingBoxQueryBuilder builder = new GeoBoundingBoxQueryBuilder(fieldName);
builder.setCorners(topLeft, bottomRight);
builder.queryName(queryName);
Expand Down Expand Up @@ -493,4 +459,69 @@ protected int doHashCode() {
public String getWriteableName() {
return NAME;
}

public static Rectangle parseBoundingBox(XContentParser parser) throws IOException, ElasticsearchParseException {
XContentParser.Token token = parser.currentToken();
if (token != XContentParser.Token.START_OBJECT) {
throw new ElasticsearchParseException("failed to parse bounding box. Expected start object but found [{}]", token);
}

double top = Double.NaN;
double bottom = Double.NaN;
double left = Double.NaN;
double right = Double.NaN;

String currentFieldName;
GeoPoint sparse = new GeoPoint();
EnvelopeBuilder envelope = null;

while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
if (token == XContentParser.Token.FIELD_NAME) {
currentFieldName = parser.currentName();
token = parser.nextToken();
if (WKT_FIELD.match(currentFieldName)) {
envelope = (EnvelopeBuilder)(GeoWKTParser.parseExpectedType(parser, GeoShapeType.ENVELOPE));
} else if (TOP_FIELD.match(currentFieldName)) {
top = parser.doubleValue();
} else if (BOTTOM_FIELD.match(currentFieldName)) {
bottom = parser.doubleValue();
} else if (LEFT_FIELD.match(currentFieldName)) {
left = parser.doubleValue();
} else if (RIGHT_FIELD.match(currentFieldName)) {
right = parser.doubleValue();
} else {
if (TOP_LEFT_FIELD.match(currentFieldName)) {
GeoUtils.parseGeoPoint(parser, sparse);
top = sparse.getLat();
left = sparse.getLon();
} else if (BOTTOM_RIGHT_FIELD.match(currentFieldName)) {
GeoUtils.parseGeoPoint(parser, sparse);
bottom = sparse.getLat();
right = sparse.getLon();
} else if (TOP_RIGHT_FIELD.match(currentFieldName)) {
GeoUtils.parseGeoPoint(parser, sparse);
top = sparse.getLat();
right = sparse.getLon();
} else if (BOTTOM_LEFT_FIELD.match(currentFieldName)) {
GeoUtils.parseGeoPoint(parser, sparse);
bottom = sparse.getLat();
left = sparse.getLon();
} else {
throw new ElasticsearchParseException("failed to parse bounding box. unexpected field [{}]", currentFieldName);
}
}
} else {
throw new ElasticsearchParseException("failed to parse bounding box. field name expected but [{}] found", token);
}
}
if (envelope != null) {
if ((Double.isNaN(top) || Double.isNaN(bottom) || Double.isNaN(left) || Double.isNaN(right)) == false) {
throw new ElasticsearchParseException("failed to parse bounding box. Conflicting definition found "
+ "using well-known text and explicit corners.");
}
org.locationtech.spatial4j.shape.Rectangle r = envelope.build();
return new Rectangle(r.getMinY(), r.getMaxY(), r.getMinX(), r.getMaxX());
}
return new Rectangle(bottom, top, left, right);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import org.elasticsearch.common.geo.parsers.GeoWKTParser;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.test.geo.RandomShapeGenerator;
import org.locationtech.spatial4j.exception.InvalidShapeException;
import org.locationtech.spatial4j.shape.Rectangle;
Expand All @@ -51,6 +52,8 @@
import java.util.List;

import static org.elasticsearch.common.geo.builders.ShapeBuilder.SPATIAL_CONTEXT;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.hasToString;

/**
* Tests for {@code GeoWKTShapeParser}
Expand Down Expand Up @@ -252,4 +255,13 @@ public void testParseGeometryCollection() throws IOException {
assertExpected(gcb.build(), gcb);
}
}

public void testUnexpectedShapeException() throws IOException {
XContentBuilder builder = toWKTContent(new PointBuilder(-1, 2), false);
XContentParser parser = createParser(builder);
parser.nextToken();
ElasticsearchParseException e = expectThrows(ElasticsearchParseException.class,
() -> GeoWKTParser.parseExpectedType(parser, GeoShapeType.POLYGON));
assertThat(e, hasToString(containsString("Expected geometry type [polygon] but found [point]")));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,50 @@ public void testFromJson() throws IOException {
assertEquals(json, GeoExecType.MEMORY, parsed.type());
}

public void testFromWKT() throws IOException {
String wkt =
"{\n" +
" \"geo_bounding_box\" : {\n" +
" \"pin.location\" : {\n" +
" \"wkt\" : \"BBOX (-74.1, -71.12, 40.73, 40.01)\"\n" +
" },\n" +
" \"validation_method\" : \"STRICT\",\n" +
" \"type\" : \"MEMORY\",\n" +
" \"ignore_unmapped\" : false,\n" +
" \"boost\" : 1.0\n" +
" }\n" +
"}";

// toXContent generates the query in geojson only; for now we need to test against the expected
// geojson generated content
String expectedJson =
"{\n" +
" \"geo_bounding_box\" : {\n" +
" \"pin.location\" : {\n" +
" \"top_left\" : [ -74.1, 40.73 ],\n" +
" \"bottom_right\" : [ -71.12, 40.01 ]\n" +
" },\n" +
" \"validation_method\" : \"STRICT\",\n" +
" \"type\" : \"MEMORY\",\n" +
" \"ignore_unmapped\" : false,\n" +
" \"boost\" : 1.0\n" +
" }\n" +
"}";

// parse with wkt
GeoBoundingBoxQueryBuilder parsed = (GeoBoundingBoxQueryBuilder) parseQuery(wkt);
// check the builder's generated geojson content against the expected json output
checkGeneratedJson(expectedJson, parsed);
double delta = 0d;
assertEquals(expectedJson, "pin.location", parsed.fieldName());
assertEquals(expectedJson, -74.1, parsed.topLeft().getLon(), delta);
assertEquals(expectedJson, 40.73, parsed.topLeft().getLat(), delta);
assertEquals(expectedJson, -71.12, parsed.bottomRight().getLon(), delta);
assertEquals(expectedJson, 40.01, parsed.bottomRight().getLat(), delta);
assertEquals(expectedJson, 1.0, parsed.boost(), delta);
assertEquals(expectedJson, GeoExecType.MEMORY, parsed.type());
}

@Override
public void testMustRewrite() throws IOException {
assumeTrue("test runs only when at least a type is registered", getCurrentTypes().length > 0);
Expand Down

0 comments on commit 5ed25f1

Please sign in to comment.