|
| 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