Skip to content

Commit bed121e

Browse files
authored
[7.x-backport] Centralize BoundingBox logic to a dedicated class (#50469)
Both geo_bounding_box query and geo_bounds aggregation have a very similar definition of a "bounding box". A lot of this logic (serialization, xcontent-parsing, etc) can be centralized instead of having separated efforts to do the same things
1 parent 694b119 commit bed121e

File tree

6 files changed

+434
-189
lines changed

6 files changed

+434
-189
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.elasticsearch.common.geo;
20+
21+
import org.elasticsearch.ElasticsearchParseException;
22+
import org.elasticsearch.common.ParseField;
23+
import org.elasticsearch.common.io.stream.StreamInput;
24+
import org.elasticsearch.common.io.stream.StreamOutput;
25+
import org.elasticsearch.common.io.stream.Writeable;
26+
import org.elasticsearch.common.xcontent.ToXContentObject;
27+
import org.elasticsearch.common.xcontent.XContentBuilder;
28+
import org.elasticsearch.common.xcontent.XContentParser;
29+
import org.elasticsearch.geometry.Geometry;
30+
import org.elasticsearch.geometry.Rectangle;
31+
import org.elasticsearch.geometry.ShapeType;
32+
import org.elasticsearch.geometry.utils.StandardValidator;
33+
import org.elasticsearch.geometry.utils.WellKnownText;
34+
35+
import java.io.IOException;
36+
import java.text.ParseException;
37+
import java.util.Objects;
38+
39+
/**
40+
* A class representing a Geo-Bounding-Box for use by Geo queries and aggregations
41+
* that deal with extents/rectangles representing rectangular areas of interest.
42+
*/
43+
public class GeoBoundingBox implements ToXContentObject, Writeable {
44+
private static final WellKnownText WKT_PARSER = new WellKnownText(true, new StandardValidator(true));
45+
static final ParseField TOP_RIGHT_FIELD = new ParseField("top_right");
46+
static final ParseField BOTTOM_LEFT_FIELD = new ParseField("bottom_left");
47+
static final ParseField TOP_FIELD = new ParseField("top");
48+
static final ParseField BOTTOM_FIELD = new ParseField("bottom");
49+
static final ParseField LEFT_FIELD = new ParseField("left");
50+
static final ParseField RIGHT_FIELD = new ParseField("right");
51+
static final ParseField WKT_FIELD = new ParseField("wkt");
52+
public static final ParseField BOUNDS_FIELD = new ParseField("bounds");
53+
public static final ParseField LAT_FIELD = new ParseField("lat");
54+
public static final ParseField LON_FIELD = new ParseField("lon");
55+
public static final ParseField TOP_LEFT_FIELD = new ParseField("top_left");
56+
public static final ParseField BOTTOM_RIGHT_FIELD = new ParseField("bottom_right");
57+
58+
private final GeoPoint topLeft;
59+
private final GeoPoint bottomRight;
60+
61+
public GeoBoundingBox(GeoPoint topLeft, GeoPoint bottomRight) {
62+
this.topLeft = topLeft;
63+
this.bottomRight = bottomRight;
64+
}
65+
66+
public GeoBoundingBox(StreamInput input) throws IOException {
67+
this.topLeft = input.readGeoPoint();
68+
this.bottomRight = input.readGeoPoint();
69+
}
70+
71+
public GeoPoint topLeft() {
72+
return topLeft;
73+
}
74+
75+
public GeoPoint bottomRight() {
76+
return bottomRight;
77+
}
78+
79+
public double top() {
80+
return topLeft.lat();
81+
}
82+
83+
public double bottom() {
84+
return bottomRight.lat();
85+
}
86+
87+
public double left() {
88+
return topLeft.lon();
89+
}
90+
91+
public double right() {
92+
return bottomRight.lon();
93+
}
94+
95+
@Override
96+
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
97+
builder.startObject(BOUNDS_FIELD.getPreferredName());
98+
toXContentFragment(builder, true);
99+
builder.endObject();
100+
return builder;
101+
}
102+
103+
public XContentBuilder toXContentFragment(XContentBuilder builder, boolean buildLatLonFields) throws IOException {
104+
if (buildLatLonFields) {
105+
builder.startObject(TOP_LEFT_FIELD.getPreferredName());
106+
builder.field(LAT_FIELD.getPreferredName(), topLeft.lat());
107+
builder.field(LON_FIELD.getPreferredName(), topLeft.lon());
108+
builder.endObject();
109+
} else {
110+
builder.array(TOP_LEFT_FIELD.getPreferredName(), topLeft.lon(), topLeft.lat());
111+
}
112+
if (buildLatLonFields) {
113+
builder.startObject(BOTTOM_RIGHT_FIELD.getPreferredName());
114+
builder.field(LAT_FIELD.getPreferredName(), bottomRight.lat());
115+
builder.field(LON_FIELD.getPreferredName(), bottomRight.lon());
116+
builder.endObject();
117+
} else {
118+
builder.array(BOTTOM_RIGHT_FIELD.getPreferredName(), bottomRight.lon(), bottomRight.lat());
119+
}
120+
return builder;
121+
}
122+
123+
@Override
124+
public void writeTo(StreamOutput out) throws IOException {
125+
out.writeGeoPoint(topLeft);
126+
out.writeGeoPoint(bottomRight);
127+
}
128+
129+
@Override
130+
public boolean equals(Object o) {
131+
if (this == o) return true;
132+
if (o == null || getClass() != o.getClass()) return false;
133+
GeoBoundingBox that = (GeoBoundingBox) o;
134+
return topLeft.equals(that.topLeft) &&
135+
bottomRight.equals(that.bottomRight);
136+
}
137+
138+
@Override
139+
public int hashCode() {
140+
return Objects.hash(topLeft, bottomRight);
141+
}
142+
143+
@Override
144+
public String toString() {
145+
return "BBOX (" + topLeft.lon() + ", " + bottomRight.lon() + ", " + topLeft.lat() + ", " + bottomRight.lat() + ")";
146+
}
147+
148+
/**
149+
* Parses the bounding box and returns bottom, top, left, right coordinates
150+
*/
151+
public static GeoBoundingBox parseBoundingBox(XContentParser parser) throws IOException, ElasticsearchParseException {
152+
XContentParser.Token token = parser.currentToken();
153+
if (token != XContentParser.Token.START_OBJECT) {
154+
throw new ElasticsearchParseException("failed to parse bounding box. Expected start object but found [{}]", token);
155+
}
156+
157+
double top = Double.NaN;
158+
double bottom = Double.NaN;
159+
double left = Double.NaN;
160+
double right = Double.NaN;
161+
162+
String currentFieldName;
163+
GeoPoint sparse = new GeoPoint();
164+
Rectangle envelope = null;
165+
166+
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
167+
if (token == XContentParser.Token.FIELD_NAME) {
168+
currentFieldName = parser.currentName();
169+
token = parser.nextToken();
170+
if (WKT_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
171+
try {
172+
Geometry geometry = WKT_PARSER.fromWKT(parser.text());
173+
if (ShapeType.ENVELOPE.equals(geometry.type()) == false) {
174+
throw new ElasticsearchParseException("failed to parse WKT bounding box. ["
175+
+ geometry.type() + "] found. expected [" + ShapeType.ENVELOPE + "]");
176+
}
177+
envelope = (Rectangle) geometry;
178+
} catch (ParseException|IllegalArgumentException e) {
179+
throw new ElasticsearchParseException("failed to parse WKT bounding box", e);
180+
}
181+
} else if (TOP_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
182+
top = parser.doubleValue();
183+
} else if (BOTTOM_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
184+
bottom = parser.doubleValue();
185+
} else if (LEFT_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
186+
left = parser.doubleValue();
187+
} else if (RIGHT_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
188+
right = parser.doubleValue();
189+
} else {
190+
if (TOP_LEFT_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
191+
GeoUtils.parseGeoPoint(parser, sparse, false, GeoUtils.EffectivePoint.TOP_LEFT);
192+
top = sparse.getLat();
193+
left = sparse.getLon();
194+
} else if (BOTTOM_RIGHT_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
195+
GeoUtils.parseGeoPoint(parser, sparse, false, GeoUtils.EffectivePoint.BOTTOM_RIGHT);
196+
bottom = sparse.getLat();
197+
right = sparse.getLon();
198+
} else if (TOP_RIGHT_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
199+
GeoUtils.parseGeoPoint(parser, sparse, false, GeoUtils.EffectivePoint.TOP_RIGHT);
200+
top = sparse.getLat();
201+
right = sparse.getLon();
202+
} else if (BOTTOM_LEFT_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
203+
GeoUtils.parseGeoPoint(parser, sparse, false, GeoUtils.EffectivePoint.BOTTOM_LEFT);
204+
bottom = sparse.getLat();
205+
left = sparse.getLon();
206+
} else {
207+
throw new ElasticsearchParseException("failed to parse bounding box. unexpected field [{}]", currentFieldName);
208+
}
209+
}
210+
} else {
211+
throw new ElasticsearchParseException("failed to parse bounding box. field name expected but [{}] found", token);
212+
}
213+
}
214+
if (envelope != null) {
215+
if (Double.isNaN(top) == false || Double.isNaN(bottom) == false || Double.isNaN(left) == false ||
216+
Double.isNaN(right) == false) {
217+
throw new ElasticsearchParseException("failed to parse bounding box. Conflicting definition found "
218+
+ "using well-known text and explicit corners.");
219+
}
220+
GeoPoint topLeft = new GeoPoint(envelope.getMaxLat(), envelope.getMinLon());
221+
GeoPoint bottomRight = new GeoPoint(envelope.getMinLat(), envelope.getMaxLon());
222+
return new GeoBoundingBox(topLeft, bottomRight);
223+
}
224+
GeoPoint topLeft = new GeoPoint(top, left);
225+
GeoPoint bottomRight = new GeoPoint(bottom, right);
226+
return new GeoBoundingBox(topLeft, bottomRight);
227+
}
228+
229+
}

0 commit comments

Comments
 (0)