diff --git a/server/src/main/java/org/elasticsearch/common/geo/EdgeTreeReader.java b/server/src/main/java/org/elasticsearch/common/geo/EdgeTreeReader.java index dc502f879aaf4..b1f1eb5a1a622 100644 --- a/server/src/main/java/org/elasticsearch/common/geo/EdgeTreeReader.java +++ b/server/src/main/java/org/elasticsearch/common/geo/EdgeTreeReader.java @@ -18,71 +18,78 @@ */ package org.elasticsearch.common.geo; -import org.apache.lucene.util.BytesRef; import org.elasticsearch.common.io.stream.ByteBufferStreamInput; import org.elasticsearch.common.io.stream.StreamInput; import java.io.IOException; -import java.nio.ByteBuffer; +import java.util.Optional; import static org.apache.lucene.geo.GeoUtils.lineCrossesLineWithBoundary; public class EdgeTreeReader { - final BytesRef bytesRef; + final ByteBufferStreamInput input; + final int startPosition; - public EdgeTreeReader(StreamInput input) throws IOException { - int treeBytesSize = input.readVInt(); - this.bytesRef = input.readBytesRef(treeBytesSize); + public EdgeTreeReader(ByteBufferStreamInput input) throws IOException { + this.startPosition = input.position(); + this.input = input; + } + + public Extent getExtent() throws IOException { + resetInputPosition(); + return new Extent(input); } /** * Returns true if the rectangle query and the edge tree's shape overlap */ public boolean containedInOrCrosses(int minX, int minY, int maxX, int maxY) throws IOException { - return this.containsBottomLeft(minX, minY, maxX, maxY) || this.crosses(minX, minY, maxX, maxY); + Extent extent = new Extent(minX, minY, maxX, maxY); + return this.containsBottomLeft(extent) || this.crosses(extent); } - boolean containsBottomLeft(int minX, int minY, int maxX, int maxY) throws IOException { - ByteBufferStreamInput input = new ByteBufferStreamInput(ByteBuffer.wrap(bytesRef.bytes, bytesRef.offset, bytesRef.length)); - int thisMinX = input.readInt(); - int thisMinY = input.readInt(); - int thisMaxX = input.readInt(); - int thisMaxY = input.readInt(); + static Optional checkExtent(StreamInput input, Extent extent) throws IOException { + Extent edgeExtent = new Extent(input); - if (thisMinY > maxY || thisMaxX < minX || thisMaxY < minY || thisMinX > maxX) { - return false; // tree and bbox-query are disjoint + if (edgeExtent.minY > extent.maxY || edgeExtent.maxX < extent.minX + || edgeExtent.maxY < extent.minY || edgeExtent.minX > extent.maxX) { + return Optional.of(false); // tree and bbox-query are disjoint } - if (minX <= thisMinX && minY <= thisMinY && maxX >= thisMaxX && maxY >= thisMaxY) { - return true; // bbox-query fully contains tree's extent. + if (extent.minX <= edgeExtent.minX && extent.minY <= edgeExtent.minY + && extent.maxX >= edgeExtent.maxX && extent.maxY >= edgeExtent.maxY) { + return Optional.of(true); // bbox-query fully contains tree's extent. } - - return containsBottomLeft(input, readRoot(input, input.position()), minX, minY, maxX, maxY); + return Optional.empty(); } - public boolean crosses(int minX, int minY, int maxX, int maxY) throws IOException { - ByteBufferStreamInput input = new ByteBufferStreamInput(ByteBuffer.wrap(bytesRef.bytes, bytesRef.offset, bytesRef.length)); - int thisMinX = input.readInt(); - int thisMinY = input.readInt(); - int thisMaxX = input.readInt(); - int thisMaxY = input.readInt(); + boolean containsBottomLeft(Extent extent) throws IOException { + resetInputPosition(); - if (thisMinY > maxY || thisMaxX < minX || thisMaxY < minY || thisMinX > maxX) { - return false; // tree and bbox-query are disjoint + Optional extentCheck = checkExtent(input, extent); + if (extentCheck.isPresent()) { + return extentCheck.get(); } - if (minX <= thisMinX && minY <= thisMinY && maxX >= thisMaxX && maxY >= thisMaxY) { - return true; // bbox-query fully contains tree's extent. + return containsBottomLeft(readRoot(input.position()), extent); + } + + public boolean crosses(Extent extent) throws IOException { + resetInputPosition(); + + Optional extentCheck = checkExtent(input, extent); + if (extentCheck.isPresent()) { + return extentCheck.get(); } - return crosses(input, readRoot(input, input.position()), minX, minY, maxX, maxY); + return crosses(readRoot(input.position()), extent); } - public Edge readRoot(ByteBufferStreamInput input, int position) throws IOException { - return readEdge(input, position); + public Edge readRoot(int position) throws IOException { + return readEdge(position); } - private static Edge readEdge(ByteBufferStreamInput input, int position) throws IOException { + private Edge readEdge(int position) throws IOException { input.position(position); int minY = input.readInt(); int maxY = input.readInt(); @@ -95,33 +102,33 @@ private static Edge readEdge(ByteBufferStreamInput input, int position) throws I } - Edge readLeft(ByteBufferStreamInput input, Edge root) throws IOException { - return readEdge(input, root.streamOffset); + Edge readLeft(Edge root) throws IOException { + return readEdge(root.streamOffset); } - Edge readRight(ByteBufferStreamInput input, Edge root) throws IOException { - return readEdge(input, root.streamOffset + root.rightOffset); + Edge readRight(Edge root) throws IOException { + return readEdge(root.streamOffset + root.rightOffset); } /** * Returns true if the bottom-left point of the rectangle query is contained within the * tree's edges. */ - private boolean containsBottomLeft(ByteBufferStreamInput input, Edge root, int minX, int minY, int maxX, int maxY) throws IOException { + private boolean containsBottomLeft(Edge root, Extent extent) throws IOException { boolean res = false; - if (root.maxY >= minY) { + if (root.maxY >= extent.minY) { // is bbox-query contained within linearRing // cast infinite ray to the right from bottom-left of bbox-query to see if it intersects edge - if (lineCrossesLineWithBoundary(root.x1, root.y1, root.x2, root.y2,minX, minY, Integer.MAX_VALUE, minY)) { + if (lineCrossesLineWithBoundary(root.x1, root.y1, root.x2, root.y2, extent.minX, extent.minY, Integer.MAX_VALUE, extent.minY)) { res = true; } if (root.rightOffset > 0) { /* has left node */ - res ^= containsBottomLeft(input, readLeft(input, root), minX, minY, maxX, maxY); + res ^= containsBottomLeft(readLeft(root), extent); } - if (root.rightOffset > 0 && maxY >= root.minY) { /* no right node if rightOffset == -1 */ - res ^= containsBottomLeft(input, readRight(input, root), minX, minY, maxX, maxY); + if (root.rightOffset > 0 && extent.maxY >= root.minY) { /* no right node if rightOffset == -1 */ + res ^= containsBottomLeft(readRight(root), extent); } } return res; @@ -130,44 +137,47 @@ private boolean containsBottomLeft(ByteBufferStreamInput input, Edge root, int m /** * Returns true if the box crosses any edge in this edge subtree * */ - private boolean crosses(ByteBufferStreamInput input, Edge root, int minX, int minY, int maxX, int maxY) throws IOException { - boolean res = false; + private boolean crosses(Edge root, Extent extent) throws IOException { // we just have to cross one edge to answer the question, so we descend the tree and return when we do. - if (root.maxY >= minY) { + if (root.maxY >= extent.minY) { // does rectangle's edges intersect or reside inside polygon's edge - if (lineCrossesLineWithBoundary(root.x1, root.y1, root.x2, root.y2, minX, minY, maxX, minY) || - lineCrossesLineWithBoundary(root.x1, root.y1, root.x2, root.y2, maxX, minY, maxX, maxY) || - lineCrossesLineWithBoundary(root.x1, root.y1, root.x2, root.y2, maxX, maxY, minX, maxY) || - lineCrossesLineWithBoundary(root.x1, root.y1, root.x2, root.y2, minX, maxY, minX, minY)) { + if (lineCrossesLineWithBoundary(root.x1, root.y1, root.x2, root.y2, + extent.minX, extent.minY, extent.maxX, extent.minY) || + lineCrossesLineWithBoundary(root.x1, root.y1, root.x2, root.y2, + extent.maxX, extent.minY, extent.maxX, extent.maxY) || + lineCrossesLineWithBoundary(root.x1, root.y1, root.x2, root.y2, + extent.maxX, extent.maxY, extent.minX, extent.maxY) || + lineCrossesLineWithBoundary(root.x1, root.y1, root.x2, root.y2, + extent.minX, extent.maxY, extent.minX, extent.minY)) { return true; } - - if (root.rightOffset > 0) { /* has left node */ - if (crosses(input, readLeft(input, root), minX, minY, maxX, maxY)) { - return true; - } + /* has left node */ + if (root.rightOffset > 0 && crosses(readLeft(root), extent)) { + return true; } - if (root.rightOffset > 0 && maxY >= root.minY) { /* no right node if rightOffset == -1 */ - if (crosses(input, readRight(input, root), minX, minY, maxX, maxY)) { - return true; - } + /* no right node if rightOffset == -1 */ + if (root.rightOffset > 0 && extent.maxY >= root.minY && crosses(readRight(root), extent)) { + return true; } } return false; } + private void resetInputPosition() throws IOException { + input.position(startPosition); + } - private static class Edge { - int streamOffset; - int x1; - int y1; - int x2; - int y2; - int minY; - int maxY; - int rightOffset; + private static final class Edge { + final int streamOffset; + final int x1; + final int y1; + final int x2; + final int y2; + final int minY; + final int maxY; + final int rightOffset; /** * Object representing an edge node read from bytes diff --git a/server/src/main/java/org/elasticsearch/common/geo/EdgeTreeWriter.java b/server/src/main/java/org/elasticsearch/common/geo/EdgeTreeWriter.java index 96eaf296057b8..82a124775ba7f 100644 --- a/server/src/main/java/org/elasticsearch/common/geo/EdgeTreeWriter.java +++ b/server/src/main/java/org/elasticsearch/common/geo/EdgeTreeWriter.java @@ -34,69 +34,44 @@ public class EdgeTreeWriter implements Writeable { */ static final int EDGE_SIZE_IN_BYTES = 28; - int minX; - int minY; - int maxX; - int maxY; + Extent extent; final Edge tree; public EdgeTreeWriter(int[] x, int[] y) { - minX = minY = Integer.MAX_VALUE; - maxX = maxY = Integer.MIN_VALUE; + int minX = Integer.MAX_VALUE; + int minY = Integer.MAX_VALUE; + int maxX = Integer.MIN_VALUE; + int maxY = Integer.MIN_VALUE; Edge edges[] = new Edge[y.length - 1]; for (int i = 1; i < y.length; i++) { int y1 = y[i-1]; int x1 = x[i-1]; int y2 = y[i]; int x2 = x[i]; - int minY, maxY; + int edgeMinY, edgeMaxY; if (y1 < y2) { - minY = y1; - maxY = y2; + edgeMinY = y1; + edgeMaxY = y2; } else { - minY = y2; - maxY = y1; + edgeMinY = y2; + edgeMaxY = y1; } - edges[i - 1] = new Edge(x1, y1, x2, y2, minY, maxY); - this.minX = Math.min(this.minX, Math.min(x1, x2)); - this.minY = Math.min(this.minY, Math.min(y1, y2)); - this.maxX = Math.max(this.maxX, Math.max(x1, x2)); - this.maxY = Math.max(this.maxY, Math.max(y1, y2)); + edges[i - 1] = new Edge(x1, y1, x2, y2, edgeMinY, edgeMaxY); + minX = Math.min(minX, Math.min(x1, x2)); + minY = Math.min(minY, Math.min(y1, y2)); + maxX = Math.max(maxX, Math.max(x1, x2)); + maxY = Math.max(maxY, Math.max(y1, y2)); } Arrays.sort(edges); + this.extent = new Extent(minX, minY, maxX, maxY); this.tree = createTree(edges, 0, edges.length - 1); } @Override public void writeTo(StreamOutput out) throws IOException { - out.writeVInt(4 * 4 + EDGE_SIZE_IN_BYTES * tree.size); - out.writeInt(minX); - out.writeInt(minY); - out.writeInt(maxX); - out.writeInt(maxY); - writeTree(tree, out); - } - - private void writeTree(Edge edge, StreamOutput output) throws IOException { - if (edge == null) { - return; - } - output.writeInt(edge.minY); - output.writeInt(edge.maxY); - output.writeInt(edge.x1); - output.writeInt(edge.y1); - output.writeInt(edge.x2); - output.writeInt(edge.y2); - // left node is next node, write offset of right node - if (edge.left != null) { - output.writeInt(edge.left.size * EDGE_SIZE_IN_BYTES); - } else if (edge.right == null){ - output.writeInt(-1); - } else { - output.writeInt(0); - } - writeTree(edge.left, output); - writeTree(edge.right, output); + //out.writeVInt(4 * 4 + EDGE_SIZE_IN_BYTES * tree.size); + extent.writeTo(out); + tree.writeTo(out); } private static Edge createTree(Edge edges[], int low, int high) { @@ -126,7 +101,7 @@ private static Edge createTree(Edge edges[], int low, int high) { /** * Object representing an in-memory edge-tree to be serialized */ - static class Edge implements Comparable { + static class Edge implements Comparable, Writeable { final int x1; final int y1; final int x2; @@ -154,5 +129,29 @@ public int compareTo(Edge other) { } return ret; } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeInt(minY); + out.writeInt(maxY); + out.writeInt(x1); + out.writeInt(y1); + out.writeInt(x2); + out.writeInt(y2); + // left node is next node, write offset of right node + if (left != null) { + out.writeInt(left.size * EDGE_SIZE_IN_BYTES); + } else if (right == null){ + out.writeInt(-1); + } else { + out.writeInt(0); + } + if (left != null) { + left.writeTo(out); + } + if (right != null) { + right.writeTo(out); + } + } } } diff --git a/server/src/main/java/org/elasticsearch/common/geo/Extent.java b/server/src/main/java/org/elasticsearch/common/geo/Extent.java new file mode 100644 index 0000000000000..8e5e3b19d3c5e --- /dev/null +++ b/server/src/main/java/org/elasticsearch/common/geo/Extent.java @@ -0,0 +1,72 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.elasticsearch.common.geo; + +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.io.stream.Writeable; + +import java.io.IOException; +import java.util.Objects; + +/** + * Object representing the extent of a geometry object within a + * {@link GeometryTreeWriter} and {@link EdgeTreeWriter}; + */ +final class Extent implements Writeable { + final int minX; + final int minY; + final int maxX; + final int maxY; + + Extent(int minX, int minY, int maxX, int maxY) { + this.minX = minX; + this.minY = minY; + this.maxX = maxX; + this.maxY = maxY; + } + + Extent(StreamInput input) throws IOException { + this(input.readInt(), input.readInt(), input.readInt(), input.readInt()); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeInt(minX); + out.writeInt(minY); + out.writeInt(maxX); + out.writeInt(maxY); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Extent extent = (Extent) o; + return minX == extent.minX && + minY == extent.minY && + maxX == extent.maxX && + maxY == extent.maxY; + } + + @Override + public int hashCode() { + return Objects.hash(minX, minY, maxX, maxY); + } +} diff --git a/server/src/main/java/org/elasticsearch/common/geo/GeometryTreeReader.java b/server/src/main/java/org/elasticsearch/common/geo/GeometryTreeReader.java index 53f78a2ebdba9..206e3b428be21 100644 --- a/server/src/main/java/org/elasticsearch/common/geo/GeometryTreeReader.java +++ b/server/src/main/java/org/elasticsearch/common/geo/GeometryTreeReader.java @@ -24,6 +24,7 @@ import java.io.IOException; import java.nio.ByteBuffer; +import java.util.Optional; /** * A tree reader. @@ -33,28 +34,36 @@ */ public class GeometryTreeReader { - private final BytesRef bytesRef; + private final ByteBufferStreamInput input; public GeometryTreeReader(BytesRef bytesRef) { - this.bytesRef = bytesRef; + this.input = new ByteBufferStreamInput(ByteBuffer.wrap(bytesRef.bytes, bytesRef.offset, bytesRef.length)); } - public boolean containedInOrCrosses(int minLon, int minLat, int maxLon, int maxLat) throws IOException { - ByteBufferStreamInput input = new ByteBufferStreamInput( - ByteBuffer.wrap(bytesRef.bytes, bytesRef.offset, bytesRef.length)); + public Extent getExtent() throws IOException { + input.position(0); boolean hasExtent = input.readBoolean(); if (hasExtent) { - int thisMinLon = input.readInt(); - int thisMinLat = input.readInt(); - int thisMaxLon = input.readInt(); - int thisMaxLat = input.readInt(); - - if (thisMinLat > maxLat || thisMaxLon < minLon || thisMaxLat < minLat || thisMinLon > maxLon) { - return false; // tree and bbox-query are disjoint - } + return new Extent(input); + } + assert input.readVInt() == 1; + ShapeType shapeType = input.readEnum(ShapeType.class); + if (ShapeType.POLYGON.equals(shapeType)) { + EdgeTreeReader reader = new EdgeTreeReader(input); + return reader.getExtent(); + } else { + throw new UnsupportedOperationException("only polygons supported -- TODO"); + } + } - if (minLon <= thisMinLon && minLat <= thisMinLat && maxLon >= thisMaxLon && maxLat >= thisMaxLat) { - return true; // bbox-query fully contains tree's extent. + public boolean containedInOrCrosses(int minLon, int minLat, int maxLon, int maxLat) throws IOException { + input.position(0); + boolean hasExtent = input.readBoolean(); + if (hasExtent) { + Optional extentCheck = EdgeTreeReader.checkExtent(input, + new Extent(minLon, minLat, maxLon, maxLat)); + if (extentCheck.isPresent()) { + return extentCheck.get(); } } diff --git a/server/src/main/java/org/elasticsearch/common/geo/GeometryTreeWriter.java b/server/src/main/java/org/elasticsearch/common/geo/GeometryTreeWriter.java index 42bf9c3d951e3..35b85d359bf14 100644 --- a/server/src/main/java/org/elasticsearch/common/geo/GeometryTreeWriter.java +++ b/server/src/main/java/org/elasticsearch/common/geo/GeometryTreeWriter.java @@ -87,10 +87,10 @@ class GeometryTreeBuilder implements GeometryVisitor { } private void addWriter(EdgeTreeWriter writer) { - minLon = Math.min(minLon, writer.minX); - minLat = Math.min(minLat, writer.minY); - maxLon = Math.max(maxLon, writer.maxX); - maxLat = Math.max(maxLat, writer.maxY); + minLon = Math.min(minLon, writer.extent.minX); + minLat = Math.min(minLat, writer.extent.minY); + maxLon = Math.max(maxLon, writer.extent.maxX); + maxLat = Math.max(maxLat, writer.extent.maxY); shapeWriters.add(writer); } diff --git a/server/src/test/java/org/elasticsearch/common/geo/EdgeTreeTests.java b/server/src/test/java/org/elasticsearch/common/geo/EdgeTreeTests.java index 38f2d6b669e8d..814c4b5736612 100644 --- a/server/src/test/java/org/elasticsearch/common/geo/EdgeTreeTests.java +++ b/server/src/test/java/org/elasticsearch/common/geo/EdgeTreeTests.java @@ -19,14 +19,17 @@ package org.elasticsearch.common.geo; import org.elasticsearch.common.geo.builders.ShapeBuilder; +import org.elasticsearch.common.io.stream.ByteBufferStreamInput; import org.elasticsearch.common.io.stream.BytesStreamOutput; -import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.geo.geometry.Polygon; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.geo.RandomShapeGenerator; import org.locationtech.spatial4j.shape.Rectangle; import java.io.IOException; +import java.nio.ByteBuffer; + +import static org.hamcrest.Matchers.equalTo; public class EdgeTreeTests extends ESTestCase { @@ -42,7 +45,7 @@ public void testRectangleShape() throws IOException { BytesStreamOutput output = new BytesStreamOutput(); writer.writeTo(output); output.close(); - EdgeTreeReader reader = new EdgeTreeReader(StreamInput.wrap(output.bytes().toBytesRef().bytes)); + EdgeTreeReader reader = new EdgeTreeReader(new ByteBufferStreamInput(ByteBuffer.wrap(output.bytes().toBytesRef().bytes))); // box-query touches bottom-left corner assertTrue(reader.containedInOrCrosses(minX - randomIntBetween(1, 180), minY - randomIntBetween(1, 180), minX, minY)); @@ -99,7 +102,8 @@ public void testSimplePolygon() throws IOException { BytesStreamOutput output = new BytesStreamOutput(); writer.writeTo(output); output.close(); - EdgeTreeReader reader = new EdgeTreeReader(StreamInput.wrap(output.bytes().toBytesRef().bytes)); + EdgeTreeReader reader = new EdgeTreeReader(new ByteBufferStreamInput(ByteBuffer.wrap(output.bytes().toBytesRef().bytes))); + assertThat(reader.getExtent(), equalTo(new Extent(minXBox, minYBox, maxXBox, maxYBox))); // polygon fully contained within box assertTrue(reader.containedInOrCrosses(minXBox, minYBox, maxXBox, maxYBox)); // containedInOrCrosses @@ -130,8 +134,8 @@ public void testPacMan() throws Exception { BytesStreamOutput output = new BytesStreamOutput(); writer.writeTo(output); output.close(); - EdgeTreeReader reader = new EdgeTreeReader(StreamInput.wrap(output.bytes().toBytesRef().bytes)); - assertTrue(reader.containsBottomLeft(xMin, yMin, xMax, yMax)); + EdgeTreeReader reader = new EdgeTreeReader(new ByteBufferStreamInput(ByteBuffer.wrap(output.bytes().toBytesRef().bytes))); + assertTrue(reader.containsBottomLeft(new Extent(xMin, yMin, xMax, yMax))); } private int[] asIntArray(double[] doub) { diff --git a/server/src/test/java/org/elasticsearch/common/geo/GeometryTreeTests.java b/server/src/test/java/org/elasticsearch/common/geo/GeometryTreeTests.java index 8df213820cd3d..0a33149c7e989 100644 --- a/server/src/test/java/org/elasticsearch/common/geo/GeometryTreeTests.java +++ b/server/src/test/java/org/elasticsearch/common/geo/GeometryTreeTests.java @@ -26,6 +26,8 @@ import java.io.IOException; import java.util.Collections; +import static org.hamcrest.Matchers.equalTo; + public class GeometryTreeTests extends ESTestCase { public void testRectangleShape() throws IOException { @@ -43,6 +45,8 @@ public void testRectangleShape() throws IOException { output.close(); GeometryTreeReader reader = new GeometryTreeReader(output.bytes().toBytesRef()); + assertThat(reader.getExtent(), equalTo(new Extent(minX, minY, maxX, maxY))); + // box-query touches bottom-left corner assertTrue(reader.containedInOrCrosses(minX - randomIntBetween(1, 180), minY - randomIntBetween(1, 180), minX, minY)); // box-query touches bottom-right corner