From 9eabd129e1e295e5a143142678b5d32b30cb1062 Mon Sep 17 00:00:00 2001 From: Tal Levy Date: Sun, 17 Mar 2019 19:58:55 -0700 Subject: [PATCH 1/8] introduce EdgeTree writer and reader This commit introduces a new data-structure for reading and writing EdgeTrees that write/read serialized versions of the tree. This tree is the basis of Polygon trees that will contain representation of any holes in the more complex polygon --- .../common/geo/LinearRingEdgeTreeReader.java | 148 +++++++++++++++++ .../common/geo/LinearRingEdgeTreeWriter.java | 152 ++++++++++++++++++ .../common/geo/EdgeTreeTests.java | 130 +++++++++++++++ 3 files changed, 430 insertions(+) create mode 100644 server/src/main/java/org/elasticsearch/common/geo/LinearRingEdgeTreeReader.java create mode 100644 server/src/main/java/org/elasticsearch/common/geo/LinearRingEdgeTreeWriter.java create mode 100644 server/src/test/java/org/elasticsearch/common/geo/EdgeTreeTests.java diff --git a/server/src/main/java/org/elasticsearch/common/geo/LinearRingEdgeTreeReader.java b/server/src/main/java/org/elasticsearch/common/geo/LinearRingEdgeTreeReader.java new file mode 100644 index 0000000000000..ba5426c64169c --- /dev/null +++ b/server/src/main/java/org/elasticsearch/common/geo/LinearRingEdgeTreeReader.java @@ -0,0 +1,148 @@ +/* + * 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.apache.lucene.index.PointValues; +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 static org.apache.lucene.geo.GeoUtils.lineRelateLine; + +public class LinearRingEdgeTreeReader { + final BytesRef bytesRef; + + public LinearRingEdgeTreeReader(BytesRef bytesRef) { + this.bytesRef = bytesRef; + } + + /** + * even partially + */ + public boolean containedIn(int minX, int minY, int maxX, int maxY) throws IOException { + StreamInput input = new ByteBufferStreamInput(ByteBuffer.wrap(bytesRef.bytes, bytesRef.offset, bytesRef.length)); + int[] extent = readExtent(input); + int thisMinX = extent[0]; + int thisMinY = extent[1]; + int thisMaxX = extent[2]; + int thisMaxY = extent[3]; + + if (thisMinY > maxY || thisMaxX < minX || thisMaxY < minY || thisMinX > maxX) { + return 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. + } + + Edge root = readRoot(input); + return root.insideOrCrosses(minX, minY, maxX, maxY); + } + + public int[] readExtent(StreamInput input) throws IOException { + int minX = input.readInt(); + int minY = input.readInt(); + int maxX = input.readInt(); + int maxY = input.readInt(); + return new int[] { minX, minY, maxX, maxY }; + } + + public Edge readRoot(StreamInput input) throws IOException { + return Edge.readEdge(input); + } + + private static class Edge { + StreamInput input; + int x1; + int y1; + int x2; + int y2; + int minY; + int maxY; + int rightOffset; + + Edge(StreamInput input, int x1, int y1, int x2, int y2, int minY, int maxY, int rightOffset) { + this.input = input; + this.x1 = x1; + this.y1 = y1; + this.x2 = x2; + this.y2 = y2; + this.minY = minY; + this.maxY = maxY; + this.rightOffset = rightOffset; + } + + Edge readLeft() throws IOException { + return readEdge(input); + } + + Edge readRight() throws IOException { + input.skip(rightOffset); + return readEdge(input); + } + + /** Returns true if the box crosses any edge in this edge subtree */ + private boolean insideOrCrosses(int minX, int minY, int maxX, int maxY) throws IOException { + // we just have to cross one edge to answer the question, so we descend the tree and return when we do. + if (this.maxY >= minY) { + // is bbox-query contained within linearRing + // cast infinite ray to the right from bottom-left and top-right of bbox-query to see if it intersects edge + if (lineRelateLine(x1, y1, x2, y2, minX, minY, Integer.MAX_VALUE, minY) != PointValues.Relation.CELL_OUTSIDE_QUERY || + lineRelateLine(x1, y1, x2, y2, maxX, maxY, Integer.MAX_VALUE, maxY) != PointValues.Relation.CELL_OUTSIDE_QUERY) { + return true; + } + + // does rectangle's edges intersect or reside inside polygon's edge + if (lineRelateLine(x1, y1, x2, y2, minX, minY, maxX, minY) != PointValues.Relation.CELL_OUTSIDE_QUERY || + lineRelateLine(x1, y1, x2, y2, maxX, minY, maxX, maxY) != PointValues.Relation.CELL_OUTSIDE_QUERY || + lineRelateLine(x1, y1, x2, y2, maxX, maxY, minX, maxY) != PointValues.Relation.CELL_OUTSIDE_QUERY || + lineRelateLine(x1, y1, x2, y2, minX, maxY, minX, minY) != PointValues.Relation.CELL_OUTSIDE_QUERY) { + return true; + } + + if (rightOffset > 1) { /* has left node */ + if (readLeft().insideOrCrosses(minX, minY, maxX, maxY)) { + return true; + } + } + + if (rightOffset > 0 && maxY >= this.minY) { /* no right node if rightOffset == -1 */ + if (readRight().insideOrCrosses(minX, minY, maxX, maxY)) { + return true; + } + } + } + return false; + } + + private static Edge readEdge(StreamInput input) throws IOException { + int minY = input.readInt(); + int maxY = input.readInt(); + int x1 = input.readInt(); + int y1 = input.readInt(); + int x2 = input.readInt(); + int y2 = input.readInt(); + int rightOffset = input.readInt(); + return new Edge(input, x1, y1, x2, y2, minY, maxY, rightOffset); + } + } +} diff --git a/server/src/main/java/org/elasticsearch/common/geo/LinearRingEdgeTreeWriter.java b/server/src/main/java/org/elasticsearch/common/geo/LinearRingEdgeTreeWriter.java new file mode 100644 index 0000000000000..c51b9978316bb --- /dev/null +++ b/server/src/main/java/org/elasticsearch/common/geo/LinearRingEdgeTreeWriter.java @@ -0,0 +1,152 @@ +/* + * 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.apache.lucene.util.BytesRef; +import org.elasticsearch.common.io.stream.BytesStreamOutput; +import org.elasticsearch.common.io.stream.StreamOutput; + +import java.io.IOException; +import java.util.Arrays; + +public class LinearRingEdgeTreeWriter { + + /** + * | minY | maxY | x1 | y1 | x2 | y2 | right_offset | + */ + static final int EDGE_SIZE_IN_BYTES = 28; + + int minX = Integer.MAX_VALUE; + int minY = Integer.MAX_VALUE; + int maxX = Integer.MIN_VALUE; + int maxY = Integer.MIN_VALUE; + final Edge tree; + + public LinearRingEdgeTreeWriter(int[] x, int[] y) throws IOException { + 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; + if (y1 < y2) { + minY = y1; + maxY = y2; + } else { + minY = y2; + maxY = 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)); + } + Arrays.sort(edges); + this.tree = createTree(edges, 0, edges.length - 1); + } + + public BytesRef toBytesRef() throws IOException { + BytesStreamOutput output = new BytesStreamOutput(4 * 4 + EDGE_SIZE_IN_BYTES * tree.size); + output.writeInt(minX); + output.writeInt(minY); + output.writeInt(maxX); + output.writeInt(maxY); + writeTree(tree, output); + output.close(); + return output.bytes().toBytesRef(); + } + + 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(1); + } + writeTree(edge.left, output); + writeTree(edge.right, output); + } + + private static Edge createTree(Edge edges[], int low, int high) { + if (low > high) { + return null; + } + // add midpoint + int mid = (low + high) >>> 1; + Edge newNode = edges[mid]; + newNode.size = 1; + // add children + newNode.left = createTree(edges, low, mid - 1); + newNode.right = createTree(edges, mid + 1, high); + // pull up max values to this node + // and node count + if (newNode.left != null) { + newNode.maxY = Math.max(newNode.maxY, newNode.left.maxY); + newNode.size += newNode.left.size; + } + if (newNode.right != null) { + newNode.maxY = Math.max(newNode.maxY, newNode.right.maxY); + newNode.size += newNode.right.size; + } + return newNode; + } + + private static class Edge implements Comparable { + final int x1; + final int y1; + final int x2; + final int y2; + int minY; + int maxY; + int size; + Edge left; + Edge right; + + Edge(int x1, int y1, int x2, int y2, int minY, int maxY) { + this.x1 = x1; + this.y1 = y1; + this.x2 = x2; + this.y2 = y2; + this.minY = minY; + this.maxY = maxY; + } + + @Override + public int compareTo(Edge other) { + int ret = Integer.compare(minY, other.minY); + if (ret == 0) { + ret = Integer.compare(maxY, other.maxY); + } + return ret; + } + } +} diff --git a/server/src/test/java/org/elasticsearch/common/geo/EdgeTreeTests.java b/server/src/test/java/org/elasticsearch/common/geo/EdgeTreeTests.java new file mode 100644 index 0000000000000..d7b8e9edb3266 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/common/geo/EdgeTreeTests.java @@ -0,0 +1,130 @@ +/* + * 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.apache.lucene.util.BytesRef; +import org.elasticsearch.common.geo.builders.ShapeBuilder; +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; + +public class EdgeTreeTests extends ESTestCase { + + public void testRectangleShape() throws IOException { + for (int i = 0; i < 1000; i++) { + int minX = randomIntBetween(-180, 170); + int maxX = randomIntBetween(minX + 10, 180); + int minY = randomIntBetween(-180, 170); + int maxY = randomIntBetween(minY + 10, 180); + int[] x = new int[]{minX, maxX, maxX, minX, minX}; + int[] y = new int[]{minY, minY, maxY, maxY, minY}; + LinearRingEdgeTreeWriter writer = new LinearRingEdgeTreeWriter(x, y); + BytesRef bytes = writer.toBytesRef(); + LinearRingEdgeTreeReader reader = new LinearRingEdgeTreeReader(bytes); + + // box-query touches bottom-left corner + assertTrue(reader.containedIn(minX - randomIntBetween(1, 180), minY - randomIntBetween(1, 180), minX, minY)); + // box-query touches bottom-right corner + assertTrue(reader.containedIn(maxX, minY - randomIntBetween(1, 180), maxX + randomIntBetween(1, 180), minY)); + // box-query touches top-right corner + assertTrue(reader.containedIn(maxX, maxY, maxX + randomIntBetween(1, 180), maxY + randomIntBetween(1, 180))); + // box-query touches top-left corner + assertTrue(reader.containedIn(minX - randomIntBetween(1, 180), maxY, minX, maxY + randomIntBetween(1, 180))); + + // box-query fully-enclosed inside rectangle + assertTrue(reader.containedIn((3 * minX + maxX) / 4, (3 * minY + maxY) / 4, (3 * maxX + minX) / 4, (3 * maxY + minY) / 4)); + + // box-query fully-contains poly + assertTrue(reader.containedIn(minX - randomIntBetween(1, 180), minY - randomIntBetween(1, 180), + maxX + randomIntBetween(1, 180), maxY + randomIntBetween(1, 180))); + + // box-query half-in-half-out-right + assertTrue(reader.containedIn((3 * minX + maxX) / 4, (3 * minY + maxY) / 4, maxX + randomIntBetween(1, 1000), (3 * maxY + minY) / 4)); + // box-query half-in-half-out-left + assertTrue(reader.containedIn(minX - randomIntBetween(1, 1000), (3 * minY + maxY) / 4, (3 * maxX + minX) / 4, (3 * maxY + minY) / 4)); + // box-query half-in-half-out-top + assertTrue(reader.containedIn((3 * minX + maxX) / 4, (3 * minY + maxY) / 4, maxX + randomIntBetween(1, 1000), maxY + randomIntBetween(1, 1000))); + // box-query half-in-half-out-bottom + assertTrue(reader.containedIn((3 * minX + maxX) / 4, minY - randomIntBetween(1, 1000), maxX + randomIntBetween(1, 1000), (3 * maxY + minY) / 4)); + + // box-query outside to the right + assertFalse(reader.containedIn(maxX + randomIntBetween(1, 1000), minY, maxX + randomIntBetween(1001, 2000), maxY)); + // box-query outside to the left + assertFalse(reader.containedIn(maxX - randomIntBetween(1001, 2000), minY, minX - randomIntBetween(1, 1000), maxY)); + // box-query outside to the top + assertFalse(reader.containedIn(minX, maxY + randomIntBetween(1, 1000), maxX, maxY + randomIntBetween(1001, 2000))); + // box-query outside to the bottom + assertFalse(reader.containedIn(minX, minY - randomIntBetween(1001, 2000), maxX, minY - randomIntBetween(1, 1000))); + } + } + + public void testSimplePolygon() throws IOException { + for (int iter = 0; iter < 1000; iter++) { + ShapeBuilder builder = RandomShapeGenerator.createShape(random(), RandomShapeGenerator.ShapeType.POLYGON); + Polygon geo = (Polygon) builder.buildGeometry(); + Rectangle box = builder.buildS4J().getBoundingBox(); + int minXBox = (int) box.getMinX(); + int minYBox = (int) box.getMinY(); + int maxXBox = (int) box.getMaxX(); + int maxYBox = (int) box.getMaxY(); + + int[] x = asIntArray(geo.getPolygon().getLons()); + int[] y = asIntArray(geo.getPolygon().getLats()); + + LinearRingEdgeTreeWriter writer = new LinearRingEdgeTreeWriter(x, y); + LinearRingEdgeTreeReader reader = new LinearRingEdgeTreeReader(writer.toBytesRef()); + // box contained within polygon + assertTrue(reader.containedIn((minXBox + maxXBox) / 2, (minYBox + maxYBox) / 2, maxXBox - 1, maxYBox - 1)); + // fully contained within box + assertTrue(reader.containedIn(minXBox, minYBox, maxXBox, maxYBox)); + // crosses + assertTrue(reader.containedIn(minXBox, minYBox, maxXBox, maxYBox - 1)); + // does not cross + assertFalse(reader.containedIn(maxXBox + 1, maxYBox + 1, maxXBox + 10, maxYBox + 10)); + } + } + + public void testPacMan() throws Exception { + // pacman + int[] px = {0, 10, 10, 0, -8, -10, -8, 0, 10, 10, 0}; + int[] py = {0, 5, 9, 10, 9, 0, -9, -10, -9, -5, 0}; + + // candidate crosses cell + int xMin = 2;//-5; + int xMax = 11;//0.000001; + int yMin = -1;//0; + int yMax = 1;//5; + + // test cell crossing poly + LinearRingEdgeTreeWriter writer = new LinearRingEdgeTreeWriter(px, py); + LinearRingEdgeTreeReader reader = new LinearRingEdgeTreeReader(writer.toBytesRef()); + assertTrue(reader.containedIn(xMin, yMin, xMax, yMax)); + } + + private int[] asIntArray(double[] doub) { + int[] intArr = new int[doub.length]; + for (int i = 0; i < intArr.length; i++) { + intArr[i] = (int) doub[i]; + } + return intArr; + } +} From bcc7b68e5e3e75f5b27432eea2997c74019d67f2 Mon Sep 17 00:00:00 2001 From: Tal Levy Date: Fri, 5 Apr 2019 09:11:03 -0700 Subject: [PATCH 2/8] next step --- .../common/geo/LinearRingEdgeTreeReader.java | 88 ++++++++++++++----- .../common/geo/LinearRingEdgeTreeWriter.java | 2 +- .../io/stream/ByteBufferStreamInput.java | 8 ++ .../common/geo/EdgeTreeTests.java | 26 ++++-- 4 files changed, 95 insertions(+), 29 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/common/geo/LinearRingEdgeTreeReader.java b/server/src/main/java/org/elasticsearch/common/geo/LinearRingEdgeTreeReader.java index ba5426c64169c..cd97b416724ce 100644 --- a/server/src/main/java/org/elasticsearch/common/geo/LinearRingEdgeTreeReader.java +++ b/server/src/main/java/org/elasticsearch/common/geo/LinearRingEdgeTreeReader.java @@ -35,11 +35,12 @@ public LinearRingEdgeTreeReader(BytesRef bytesRef) { this.bytesRef = bytesRef; } - /** - * even partially - */ + public boolean containedInOrCrosses(int minX, int minY, int maxX, int maxY) throws IOException { + return this.containedIn(minX, minY, maxX, maxY) || this.crosses(minX, minY, maxX, maxY); + } + public boolean containedIn(int minX, int minY, int maxX, int maxY) throws IOException { - StreamInput input = new ByteBufferStreamInput(ByteBuffer.wrap(bytesRef.bytes, bytesRef.offset, bytesRef.length)); + ByteBufferStreamInput input = new ByteBufferStreamInput(ByteBuffer.wrap(bytesRef.bytes, bytesRef.offset, bytesRef.length)); int[] extent = readExtent(input); int thisMinX = extent[0]; int thisMinY = extent[1]; @@ -54,11 +55,29 @@ public boolean containedIn(int minX, int minY, int maxX, int maxY) throws IOExce return true; // bbox-query fully contains tree's extent. } - Edge root = readRoot(input); - return root.insideOrCrosses(minX, minY, maxX, maxY); + return readRoot(input, input.position()).contains(minX, minY, maxX, maxY); } - public int[] readExtent(StreamInput input) throws IOException { + 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[] extent = readExtent(input); + int thisMinX = extent[0]; + int thisMinY = extent[1]; + int thisMaxX = extent[2]; + int thisMaxY = extent[3]; + + if (thisMinY > maxY || thisMaxX < minX || thisMaxY < minY || thisMinX > maxX) { + return 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. + } + + return readRoot(input, input.position()).crosses(minX, minY, maxX, maxY); + } + + public int[] readExtent(ByteBufferStreamInput input) throws IOException { int minX = input.readInt(); int minY = input.readInt(); int maxX = input.readInt(); @@ -66,12 +85,13 @@ public int[] readExtent(StreamInput input) throws IOException { return new int[] { minX, minY, maxX, maxY }; } - public Edge readRoot(StreamInput input) throws IOException { - return Edge.readEdge(input); + public Edge readRoot(ByteBufferStreamInput input, int position) throws IOException { + return Edge.readEdge(input, position); } private static class Edge { - StreamInput input; + ByteBufferStreamInput input; + int streamOffset; int x1; int y1; int x2; @@ -80,8 +100,9 @@ private static class Edge { int maxY; int rightOffset; - Edge(StreamInput input, int x1, int y1, int x2, int y2, int minY, int maxY, int rightOffset) { + Edge(ByteBufferStreamInput input, int streamOffset, int x1, int y1, int x2, int y2, int minY, int maxY, int rightOffset) { this.input = input; + this.streamOffset = streamOffset; this.x1 = x1; this.y1 = y1; this.x2 = x2; @@ -92,24 +113,44 @@ private static class Edge { } Edge readLeft() throws IOException { - return readEdge(input); + return readEdge(input, streamOffset); } Edge readRight() throws IOException { - input.skip(rightOffset); - return readEdge(input); + return readEdge(input, streamOffset + rightOffset); } - /** Returns true if the box crosses any edge in this edge subtree */ - private boolean insideOrCrosses(int minX, int minY, int maxX, int maxY) throws IOException { - // we just have to cross one edge to answer the question, so we descend the tree and return when we do. + private boolean contains(int minX, int minY, int maxX, int maxY) throws IOException { + boolean res = false; if (this.maxY >= minY) { // is bbox-query contained within linearRing // cast infinite ray to the right from bottom-left and top-right of bbox-query to see if it intersects edge + +// boolean collinear = lineRelateLine(x1, y1, x2, y2, minX, minY, Integer.MAX_VALUE, minY) == PointValues.Relation.CELL_INSIDE_QUERY +// || lineRelateLine(x1, y1, x2, y2, maxX, maxY, Integer.MAX_VALUE, maxY) == PointValues.Relation.CELL_INSIDE_QUERY; +// boolean crosses = lineRelateLine(x1, y1, x2, y2, minX, minY, Integer.MAX_VALUE, minY) == PointValues.Relation.CELL_CROSSES_QUERY +// || lineRelateLine(x1, y1, x2, y2, maxX, maxY, Integer.MAX_VALUE, maxY) == PointValues.Relation.CELL_CROSSES_QUERY; + if (lineRelateLine(x1, y1, x2, y2, minX, minY, Integer.MAX_VALUE, minY) != PointValues.Relation.CELL_OUTSIDE_QUERY || lineRelateLine(x1, y1, x2, y2, maxX, maxY, Integer.MAX_VALUE, maxY) != PointValues.Relation.CELL_OUTSIDE_QUERY) { - return true; + res = true; + } + if (rightOffset > 0) { /* has left node */ + res ^= readLeft().contains(minX, minY, maxX, maxY); + } + + if (rightOffset > 0 && maxY >= this.minY) { /* no right node if rightOffset == -1 */ + res ^= readRight().contains(minX, minY, maxX, maxY); } + } + return res; + } + + /** Returns true if the box crosses any edge in this edge subtree */ + private boolean crosses(int minX, int minY, int maxX, int maxY) throws IOException { + boolean res = false; + // we just have to cross one edge to answer the question, so we descend the tree and return when we do. + if (this.maxY >= minY) { // does rectangle's edges intersect or reside inside polygon's edge if (lineRelateLine(x1, y1, x2, y2, minX, minY, maxX, minY) != PointValues.Relation.CELL_OUTSIDE_QUERY || @@ -119,14 +160,14 @@ private boolean insideOrCrosses(int minX, int minY, int maxX, int maxY) throws I return true; } - if (rightOffset > 1) { /* has left node */ - if (readLeft().insideOrCrosses(minX, minY, maxX, maxY)) { + if (rightOffset > 0) { /* has left node */ + if (readLeft().crosses(minX, minY, maxX, maxY)) { return true; } } if (rightOffset > 0 && maxY >= this.minY) { /* no right node if rightOffset == -1 */ - if (readRight().insideOrCrosses(minX, minY, maxX, maxY)) { + if (readRight().crosses(minX, minY, maxX, maxY)) { return true; } } @@ -134,7 +175,8 @@ private boolean insideOrCrosses(int minX, int minY, int maxX, int maxY) throws I return false; } - private static Edge readEdge(StreamInput input) throws IOException { + private static Edge readEdge(ByteBufferStreamInput input, int position) throws IOException { + input.position(position); int minY = input.readInt(); int maxY = input.readInt(); int x1 = input.readInt(); @@ -142,7 +184,7 @@ private static Edge readEdge(StreamInput input) throws IOException { int x2 = input.readInt(); int y2 = input.readInt(); int rightOffset = input.readInt(); - return new Edge(input, x1, y1, x2, y2, minY, maxY, rightOffset); + return new Edge(input, input.position(), x1, y1, x2, y2, minY, maxY, rightOffset); } } } diff --git a/server/src/main/java/org/elasticsearch/common/geo/LinearRingEdgeTreeWriter.java b/server/src/main/java/org/elasticsearch/common/geo/LinearRingEdgeTreeWriter.java index c51b9978316bb..5d0d484f2a5d6 100644 --- a/server/src/main/java/org/elasticsearch/common/geo/LinearRingEdgeTreeWriter.java +++ b/server/src/main/java/org/elasticsearch/common/geo/LinearRingEdgeTreeWriter.java @@ -90,7 +90,7 @@ private void writeTree(Edge edge, StreamOutput output) throws IOException { } else if (edge.right == null){ output.writeInt(-1); } else { - output.writeInt(1); + output.writeInt(0); } writeTree(edge.left, output); writeTree(edge.right, output); diff --git a/server/src/main/java/org/elasticsearch/common/io/stream/ByteBufferStreamInput.java b/server/src/main/java/org/elasticsearch/common/io/stream/ByteBufferStreamInput.java index 0668fcb85fef9..d245f3950433e 100644 --- a/server/src/main/java/org/elasticsearch/common/io/stream/ByteBufferStreamInput.java +++ b/server/src/main/java/org/elasticsearch/common/io/stream/ByteBufferStreamInput.java @@ -110,6 +110,14 @@ public long readLong() throws IOException { } } + public void position(int newPosition) throws IOException { + buffer.position(newPosition); + } + + public int position() throws IOException { + return buffer.position(); + } + @Override public void reset() throws IOException { buffer.reset(); 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 d7b8e9edb3266..35fc1f3531362 100644 --- a/server/src/test/java/org/elasticsearch/common/geo/EdgeTreeTests.java +++ b/server/src/test/java/org/elasticsearch/common/geo/EdgeTreeTests.java @@ -25,6 +25,7 @@ import org.elasticsearch.test.geo.RandomShapeGenerator; import org.locationtech.spatial4j.shape.Rectangle; +import java.io.EOFException; import java.io.IOException; public class EdgeTreeTests extends ESTestCase { @@ -42,38 +43,53 @@ public void testRectangleShape() throws IOException { LinearRingEdgeTreeReader reader = new LinearRingEdgeTreeReader(bytes); // box-query touches bottom-left corner + assertTrue(reader.crosses(minX - randomIntBetween(1, 180), minY - randomIntBetween(1, 180), minX, minY)); assertTrue(reader.containedIn(minX - randomIntBetween(1, 180), minY - randomIntBetween(1, 180), minX, minY)); // box-query touches bottom-right corner + assertTrue(reader.crosses(maxX, minY - randomIntBetween(1, 180), maxX + randomIntBetween(1, 180), minY)); assertTrue(reader.containedIn(maxX, minY - randomIntBetween(1, 180), maxX + randomIntBetween(1, 180), minY)); // box-query touches top-right corner + assertTrue(reader.crosses(maxX, maxY, maxX + randomIntBetween(1, 180), maxY + randomIntBetween(1, 180))); assertTrue(reader.containedIn(maxX, maxY, maxX + randomIntBetween(1, 180), maxY + randomIntBetween(1, 180))); // box-query touches top-left corner - assertTrue(reader.containedIn(minX - randomIntBetween(1, 180), maxY, minX, maxY + randomIntBetween(1, 180))); + assertTrue(reader.crosses(minX - randomIntBetween(1, 180), maxY, minX, maxY + randomIntBetween(1, 180))); + + /** TODO(talevy): COME ON! Right Ray Test! */ + //assertTrue(reader.containedIn(minX - randomIntBetween(1, 180), maxY, minX, maxY + randomIntBetween(1, 180))); // box-query fully-enclosed inside rectangle assertTrue(reader.containedIn((3 * minX + maxX) / 4, (3 * minY + maxY) / 4, (3 * maxX + minX) / 4, (3 * maxY + minY) / 4)); + assertFalse(reader.crosses((3 * minX + maxX) / 4, (3 * minY + maxY) / 4, (3 * maxX + minX) / 4, (3 * maxY + minY) / 4)); // box-query fully-contains poly assertTrue(reader.containedIn(minX - randomIntBetween(1, 180), minY - randomIntBetween(1, 180), maxX + randomIntBetween(1, 180), maxY + randomIntBetween(1, 180))); // box-query half-in-half-out-right - assertTrue(reader.containedIn((3 * minX + maxX) / 4, (3 * minY + maxY) / 4, maxX + randomIntBetween(1, 1000), (3 * maxY + minY) / 4)); + // assertFalse(reader.containedIn((3 * minX + maxX) / 4, (3 * minY + maxY) / 4, maxX + randomIntBetween(1, 1000), (3 * maxY + minY) / 4)); + assertTrue(reader.crosses((3 * minX + maxX) / 4, (3 * minY + maxY) / 4, maxX + randomIntBetween(1, 1000), (3 * maxY + minY) / 4)); // box-query half-in-half-out-left - assertTrue(reader.containedIn(minX - randomIntBetween(1, 1000), (3 * minY + maxY) / 4, (3 * maxX + minX) / 4, (3 * maxY + minY) / 4)); + // assertTrue(reader.containedIn(minX - randomIntBetween(1, 1000), (3 * minY + maxY) / 4, (3 * maxX + minX) / 4, (3 * maxY + minY) / 4)); + assertTrue(reader.crosses(minX - randomIntBetween(1, 1000), (3 * minY + maxY) / 4, (3 * maxX + minX) / 4, (3 * maxY + minY) / 4)); // box-query half-in-half-out-top - assertTrue(reader.containedIn((3 * minX + maxX) / 4, (3 * minY + maxY) / 4, maxX + randomIntBetween(1, 1000), maxY + randomIntBetween(1, 1000))); + // assertTrue(reader.containedIn((3 * minX + maxX) / 4, (3 * minY + maxY) / 4, maxX + randomIntBetween(1, 1000), maxY + randomIntBetween(1, 1000))); + assertTrue(reader.crosses((3 * minX + maxX) / 4, (3 * minY + maxY) / 4, maxX + randomIntBetween(1, 1000), maxY + randomIntBetween(1, 1000))); // box-query half-in-half-out-bottom - assertTrue(reader.containedIn((3 * minX + maxX) / 4, minY - randomIntBetween(1, 1000), maxX + randomIntBetween(1, 1000), (3 * maxY + minY) / 4)); + // assertTrue(reader.containedIn((3 * minX + maxX) / 4, minY - randomIntBetween(1, 1000), maxX + randomIntBetween(1, 1000), (3 * maxY + minY) / 4)); + assertTrue(reader.crosses((3 * minX + maxX) / 4, minY - randomIntBetween(1, 1000), maxX + randomIntBetween(1, 1000), (3 * maxY + minY) / 4)); // box-query outside to the right assertFalse(reader.containedIn(maxX + randomIntBetween(1, 1000), minY, maxX + randomIntBetween(1001, 2000), maxY)); + assertFalse(reader.crosses(maxX + randomIntBetween(1, 1000), minY, maxX + randomIntBetween(1001, 2000), maxY)); // box-query outside to the left assertFalse(reader.containedIn(maxX - randomIntBetween(1001, 2000), minY, minX - randomIntBetween(1, 1000), maxY)); + assertFalse(reader.crosses(maxX - randomIntBetween(1001, 2000), minY, minX - randomIntBetween(1, 1000), maxY)); // box-query outside to the top assertFalse(reader.containedIn(minX, maxY + randomIntBetween(1, 1000), maxX, maxY + randomIntBetween(1001, 2000))); + assertFalse(reader.crosses(minX, maxY + randomIntBetween(1, 1000), maxX, maxY + randomIntBetween(1001, 2000))); // box-query outside to the bottom assertFalse(reader.containedIn(minX, minY - randomIntBetween(1001, 2000), maxX, minY - randomIntBetween(1, 1000))); + assertFalse(reader.crosses(minX, minY - randomIntBetween(1001, 2000), maxX, minY - randomIntBetween(1, 1000))); } } From 8135bf17eab3fa4d9c7bd150cf150a1da4033883 Mon Sep 17 00:00:00 2001 From: Tal Levy Date: Tue, 9 Apr 2019 16:38:42 -0700 Subject: [PATCH 3/8] iterate and add geometry tree --- .../common/geo/EdgeTreeReader.java | 184 +++++++++++++++++ ...dgeTreeWriter.java => EdgeTreeWriter.java} | 6 +- .../common/geo/GeometryTreeReader.java | 52 +++++ .../common/geo/GeometryTreeWriter.java | 147 ++++++++++++++ .../common/geo/LinearRingEdgeTreeReader.java | 190 ------------------ .../index/mapper/GeoShapeFieldMapper.java | 2 +- .../common/geo/EdgeTreeTests.java | 81 +++----- .../common/geo/GeometryTreeTests.java | 92 +++++++++ 8 files changed, 512 insertions(+), 242 deletions(-) create mode 100644 server/src/main/java/org/elasticsearch/common/geo/EdgeTreeReader.java rename server/src/main/java/org/elasticsearch/common/geo/{LinearRingEdgeTreeWriter.java => EdgeTreeWriter.java} (96%) create mode 100644 server/src/main/java/org/elasticsearch/common/geo/GeometryTreeReader.java create mode 100644 server/src/main/java/org/elasticsearch/common/geo/GeometryTreeWriter.java delete mode 100644 server/src/main/java/org/elasticsearch/common/geo/LinearRingEdgeTreeReader.java create mode 100644 server/src/test/java/org/elasticsearch/common/geo/GeometryTreeTests.java diff --git a/server/src/main/java/org/elasticsearch/common/geo/EdgeTreeReader.java b/server/src/main/java/org/elasticsearch/common/geo/EdgeTreeReader.java new file mode 100644 index 0000000000000..946760c915e4c --- /dev/null +++ b/server/src/main/java/org/elasticsearch/common/geo/EdgeTreeReader.java @@ -0,0 +1,184 @@ +/* + * 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.apache.lucene.index.PointValues; +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 static org.apache.lucene.geo.GeoUtils.lineRelateLine; + +public class EdgeTreeReader { + final BytesRef bytesRef; + + public EdgeTreeReader(BytesRef bytesRef) { + this.bytesRef = bytesRef; + } + + 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); + } + + 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[] extent = readExtent(input); + int thisMinX = extent[0]; + int thisMinY = extent[1]; + int thisMaxX = extent[2]; + int thisMaxY = extent[3]; + + if (thisMinY > maxY || thisMaxX < minX || thisMaxY < minY || thisMinX > maxX) { + return 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. + } + + return containsBottomLeft(input, readRoot(input, input.position()), minX, minY, maxX, maxY); + } + + 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[] extent = readExtent(input); + int thisMinX = extent[0]; + int thisMinY = extent[1]; + int thisMaxX = extent[2]; + int thisMaxY = extent[3]; + + if (thisMinY > maxY || thisMaxX < minX || thisMaxY < minY || thisMinX > maxX) { + return 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. + } + + return crosses(input, readRoot(input, input.position()), minX, minY, maxX, maxY); + } + + public int[] readExtent(ByteBufferStreamInput input) throws IOException { + int minX = input.readInt(); + int minY = input.readInt(); + int maxX = input.readInt(); + int maxY = input.readInt(); + return new int[] { minX, minY, maxX, maxY }; + } + + public Edge readRoot(ByteBufferStreamInput input, int position) throws IOException { + return readEdge(input, position); + } + + private static Edge readEdge(ByteBufferStreamInput input, int position) throws IOException { + input.position(position); + int minY = input.readInt(); + int maxY = input.readInt(); + int x1 = input.readInt(); + int y1 = input.readInt(); + int x2 = input.readInt(); + int y2 = input.readInt(); + int rightOffset = input.readInt(); + return new Edge(input.position(), x1, y1, x2, y2, minY, maxY, rightOffset); + } + + + Edge readLeft(ByteBufferStreamInput input, Edge root) throws IOException { + return readEdge(input, root.streamOffset); + } + + Edge readRight(ByteBufferStreamInput input, Edge root) throws IOException { + return readEdge(input, root.streamOffset + root.rightOffset); + } + + private boolean containsBottomLeft(ByteBufferStreamInput input, Edge root, int minX, int minY, int maxX, int maxY) throws IOException { + boolean res = false; + if (root.maxY >= 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 (lineRelateLine(root.x1, root.y1, root.x2, root.y2, minX, minY, Integer.MAX_VALUE, minY) != PointValues.Relation.CELL_OUTSIDE_QUERY) { + res = true; + } + + if (root.rightOffset > 0) { /* has left node */ + res ^= containsBottomLeft(input, readLeft(input, root), minX, minY, maxX, maxY); + } + + if (root.rightOffset > 0 && maxY >= root.minY) { /* no right node if rightOffset == -1 */ + res ^= containsBottomLeft(input, readRight(input, root), minX, minY, maxX, maxY); + } + } + return res; + } + + /** 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; + // 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) { + + // does rectangle's edges intersect or reside inside polygon's edge + if (lineRelateLine(root.x1, root.y1, root.x2, root.y2, minX, minY, maxX, minY) != PointValues.Relation.CELL_OUTSIDE_QUERY || + lineRelateLine(root.x1, root.y1, root.x2, root.y2, maxX, minY, maxX, maxY) != PointValues.Relation.CELL_OUTSIDE_QUERY || + lineRelateLine(root.x1, root.y1, root.x2, root.y2, maxX, maxY, minX, maxY) != PointValues.Relation.CELL_OUTSIDE_QUERY || + lineRelateLine(root.x1, root.y1, root.x2, root.y2, minX, maxY, minX, minY) != PointValues.Relation.CELL_OUTSIDE_QUERY) { + return true; + } + + if (root.rightOffset > 0) { /* has left node */ + if (crosses(input, readLeft(input, root), minX, minY, maxX, maxY)) { + 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; + } + } + } + return false; + } + + + private static class Edge { + int streamOffset; + int x1; + int y1; + int x2; + int y2; + int minY; + int maxY; + int rightOffset; + + Edge(int streamOffset, int x1, int y1, int x2, int y2, int minY, int maxY, int rightOffset) { + this.streamOffset = streamOffset; + this.x1 = x1; + this.y1 = y1; + this.x2 = x2; + this.y2 = y2; + this.minY = minY; + this.maxY = maxY; + this.rightOffset = rightOffset; + } + } +} diff --git a/server/src/main/java/org/elasticsearch/common/geo/LinearRingEdgeTreeWriter.java b/server/src/main/java/org/elasticsearch/common/geo/EdgeTreeWriter.java similarity index 96% rename from server/src/main/java/org/elasticsearch/common/geo/LinearRingEdgeTreeWriter.java rename to server/src/main/java/org/elasticsearch/common/geo/EdgeTreeWriter.java index 5d0d484f2a5d6..36df8e46cfa35 100644 --- a/server/src/main/java/org/elasticsearch/common/geo/LinearRingEdgeTreeWriter.java +++ b/server/src/main/java/org/elasticsearch/common/geo/EdgeTreeWriter.java @@ -25,7 +25,7 @@ import java.io.IOException; import java.util.Arrays; -public class LinearRingEdgeTreeWriter { +public class EdgeTreeWriter { /** * | minY | maxY | x1 | y1 | x2 | y2 | right_offset | @@ -38,7 +38,7 @@ public class LinearRingEdgeTreeWriter { int maxY = Integer.MIN_VALUE; final Edge tree; - public LinearRingEdgeTreeWriter(int[] x, int[] y) throws IOException { + public EdgeTreeWriter(int[] x, int[] y) { Edge edges[] = new Edge[y.length - 1]; for (int i = 1; i < y.length; i++) { int y1 = y[i-1]; @@ -120,7 +120,7 @@ private static Edge createTree(Edge edges[], int low, int high) { return newNode; } - private static class Edge implements Comparable { + static class Edge implements Comparable { final int x1; final int y1; final int x2; diff --git a/server/src/main/java/org/elasticsearch/common/geo/GeometryTreeReader.java b/server/src/main/java/org/elasticsearch/common/geo/GeometryTreeReader.java new file mode 100644 index 0000000000000..538ab6cacff79 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/common/geo/GeometryTreeReader.java @@ -0,0 +1,52 @@ +/* + * 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.apache.lucene.util.BytesRef; +import org.elasticsearch.common.io.stream.ByteBufferStreamInput; +import org.elasticsearch.geo.geometry.ShapeType; + +import java.io.IOException; +import java.nio.ByteBuffer; + +public class GeometryTreeReader { + + private final BytesRef bytesRef; + + public GeometryTreeReader(BytesRef bytesRef) { + this.bytesRef = bytesRef; + } + + public boolean containedInOrCrosses(int minX, int minY, int maxX, int maxY) throws IOException { + ByteBufferStreamInput input = new ByteBufferStreamInput( + ByteBuffer.wrap(bytesRef.bytes, bytesRef.offset, bytesRef.length)); + int numTrees = input.readVInt(); + for (int i = 0; i < numTrees; i++) { + ShapeType shapeType = input.readEnum(ShapeType.class); + if (ShapeType.POLYGON.equals(shapeType)) { + BytesRef treeRef = input.readBytesRef(); + EdgeTreeReader reader = new EdgeTreeReader(treeRef); + if (reader.containedInOrCrosses(minX, minY, maxX, maxY)) { + return true; + } + } + } + return false; + } +} diff --git a/server/src/main/java/org/elasticsearch/common/geo/GeometryTreeWriter.java b/server/src/main/java/org/elasticsearch/common/geo/GeometryTreeWriter.java new file mode 100644 index 0000000000000..a05c311e6ef9a --- /dev/null +++ b/server/src/main/java/org/elasticsearch/common/geo/GeometryTreeWriter.java @@ -0,0 +1,147 @@ +/* + * 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.apache.lucene.util.BytesRef; +import org.elasticsearch.common.io.stream.BytesStreamOutput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.geo.geometry.Circle; +import org.elasticsearch.geo.geometry.Geometry; +import org.elasticsearch.geo.geometry.GeometryCollection; +import org.elasticsearch.geo.geometry.GeometryVisitor; +import org.elasticsearch.geo.geometry.Line; +import org.elasticsearch.geo.geometry.LinearRing; +import org.elasticsearch.geo.geometry.MultiLine; +import org.elasticsearch.geo.geometry.MultiPoint; +import org.elasticsearch.geo.geometry.MultiPolygon; +import org.elasticsearch.geo.geometry.Point; +import org.elasticsearch.geo.geometry.Polygon; +import org.elasticsearch.geo.geometry.Rectangle; +import org.elasticsearch.geo.geometry.ShapeType; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class GeometryTreeWriter { + + private final GeometryTreeBuilder builder; + + public GeometryTreeWriter(Geometry geometry) { + builder = new GeometryTreeBuilder(); + geometry.visit(builder); + } + + public BytesRef toBytesRef() throws IOException { + BytesStreamOutput output = new BytesStreamOutput(); + output.writeVInt(builder.shapeWriters.size()); + for (EdgeTreeWriter writer : builder.shapeWriters) { + output.writeEnum(ShapeType.POLYGON); + output.writeBytesRef(writer.toBytesRef()); + } + output.close(); + return output.bytes().toBytesRef(); + } + + class GeometryTreeBuilder implements GeometryVisitor { + + private List shapeWriters; + + GeometryTreeBuilder() { + shapeWriters = new ArrayList<>(); + } + + @Override + public Void visit(GeometryCollection collection) { + for (Geometry geometry : collection) { + geometry.visit(this); + } + return null; + } + + @Override + public Void visit(Line line) { + // TODO + return null; + } + + @Override + public Void visit(MultiLine multiLine) { + for (Line line : multiLine) { + visit(line); + } + return null; + } + + @Override + public Void visit(Polygon polygon) { + // TODO (support holes) + LinearRing outerShell = polygon.getPolygon(); + shapeWriters.add(new EdgeTreeWriter(asIntArray(outerShell.getLons()), asIntArray(outerShell.getLats()))); + return null; + } + + @Override + public Void visit(MultiPolygon multiPolygon) { + for (Polygon polygon : multiPolygon) { + visit(polygon); + } + return null; + } + + @Override + public Void visit(Rectangle r) { + int[] lats = new int[] { (int) r.getMinLat(), (int) r.getMinLat(), (int) r.getMaxLat(), (int) r.getMaxLat(), (int) r.getMinLat()}; + int[] lons = new int[] { (int) r.getMinLon(), (int) r.getMaxLon(), (int) r.getMaxLon(), (int) r.getMinLon(), (int) r.getMinLon()}; + shapeWriters.add(new EdgeTreeWriter(lons, lats)); + return null; + } + + @Override + public Void visit(Point point) { + // TODO + return null; + } + + @Override + public Void visit(MultiPoint multiPoint) { + // TODO + return null; + } + + @Override + public Void visit(LinearRing ring) { + throw new IllegalArgumentException("invalid shape type found [Circle]"); + } + + @Override + public Void visit(Circle circle) { + throw new IllegalArgumentException("invalid shape type found [Circle]"); + } + + private int[] asIntArray(double[] doub) { + int[] intArr = new int[doub.length]; + for (int i = 0; i < intArr.length; i++) { + intArr[i] = (int) doub[i]; + } + return intArr; + } + } +} diff --git a/server/src/main/java/org/elasticsearch/common/geo/LinearRingEdgeTreeReader.java b/server/src/main/java/org/elasticsearch/common/geo/LinearRingEdgeTreeReader.java deleted file mode 100644 index cd97b416724ce..0000000000000 --- a/server/src/main/java/org/elasticsearch/common/geo/LinearRingEdgeTreeReader.java +++ /dev/null @@ -1,190 +0,0 @@ -/* - * 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.apache.lucene.index.PointValues; -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 static org.apache.lucene.geo.GeoUtils.lineRelateLine; - -public class LinearRingEdgeTreeReader { - final BytesRef bytesRef; - - public LinearRingEdgeTreeReader(BytesRef bytesRef) { - this.bytesRef = bytesRef; - } - - public boolean containedInOrCrosses(int minX, int minY, int maxX, int maxY) throws IOException { - return this.containedIn(minX, minY, maxX, maxY) || this.crosses(minX, minY, maxX, maxY); - } - - public boolean containedIn(int minX, int minY, int maxX, int maxY) throws IOException { - ByteBufferStreamInput input = new ByteBufferStreamInput(ByteBuffer.wrap(bytesRef.bytes, bytesRef.offset, bytesRef.length)); - int[] extent = readExtent(input); - int thisMinX = extent[0]; - int thisMinY = extent[1]; - int thisMaxX = extent[2]; - int thisMaxY = extent[3]; - - if (thisMinY > maxY || thisMaxX < minX || thisMaxY < minY || thisMinX > maxX) { - return 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. - } - - return readRoot(input, input.position()).contains(minX, minY, maxX, maxY); - } - - 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[] extent = readExtent(input); - int thisMinX = extent[0]; - int thisMinY = extent[1]; - int thisMaxX = extent[2]; - int thisMaxY = extent[3]; - - if (thisMinY > maxY || thisMaxX < minX || thisMaxY < minY || thisMinX > maxX) { - return 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. - } - - return readRoot(input, input.position()).crosses(minX, minY, maxX, maxY); - } - - public int[] readExtent(ByteBufferStreamInput input) throws IOException { - int minX = input.readInt(); - int minY = input.readInt(); - int maxX = input.readInt(); - int maxY = input.readInt(); - return new int[] { minX, minY, maxX, maxY }; - } - - public Edge readRoot(ByteBufferStreamInput input, int position) throws IOException { - return Edge.readEdge(input, position); - } - - private static class Edge { - ByteBufferStreamInput input; - int streamOffset; - int x1; - int y1; - int x2; - int y2; - int minY; - int maxY; - int rightOffset; - - Edge(ByteBufferStreamInput input, int streamOffset, int x1, int y1, int x2, int y2, int minY, int maxY, int rightOffset) { - this.input = input; - this.streamOffset = streamOffset; - this.x1 = x1; - this.y1 = y1; - this.x2 = x2; - this.y2 = y2; - this.minY = minY; - this.maxY = maxY; - this.rightOffset = rightOffset; - } - - Edge readLeft() throws IOException { - return readEdge(input, streamOffset); - } - - Edge readRight() throws IOException { - return readEdge(input, streamOffset + rightOffset); - } - - private boolean contains(int minX, int minY, int maxX, int maxY) throws IOException { - boolean res = false; - if (this.maxY >= minY) { - // is bbox-query contained within linearRing - // cast infinite ray to the right from bottom-left and top-right of bbox-query to see if it intersects edge - -// boolean collinear = lineRelateLine(x1, y1, x2, y2, minX, minY, Integer.MAX_VALUE, minY) == PointValues.Relation.CELL_INSIDE_QUERY -// || lineRelateLine(x1, y1, x2, y2, maxX, maxY, Integer.MAX_VALUE, maxY) == PointValues.Relation.CELL_INSIDE_QUERY; -// boolean crosses = lineRelateLine(x1, y1, x2, y2, minX, minY, Integer.MAX_VALUE, minY) == PointValues.Relation.CELL_CROSSES_QUERY -// || lineRelateLine(x1, y1, x2, y2, maxX, maxY, Integer.MAX_VALUE, maxY) == PointValues.Relation.CELL_CROSSES_QUERY; - - if (lineRelateLine(x1, y1, x2, y2, minX, minY, Integer.MAX_VALUE, minY) != PointValues.Relation.CELL_OUTSIDE_QUERY || - lineRelateLine(x1, y1, x2, y2, maxX, maxY, Integer.MAX_VALUE, maxY) != PointValues.Relation.CELL_OUTSIDE_QUERY) { - res = true; - } - if (rightOffset > 0) { /* has left node */ - res ^= readLeft().contains(minX, minY, maxX, maxY); - } - - if (rightOffset > 0 && maxY >= this.minY) { /* no right node if rightOffset == -1 */ - res ^= readRight().contains(minX, minY, maxX, maxY); - } - } - return res; - } - - /** Returns true if the box crosses any edge in this edge subtree */ - private boolean crosses(int minX, int minY, int maxX, int maxY) throws IOException { - boolean res = false; - // we just have to cross one edge to answer the question, so we descend the tree and return when we do. - if (this.maxY >= minY) { - - // does rectangle's edges intersect or reside inside polygon's edge - if (lineRelateLine(x1, y1, x2, y2, minX, minY, maxX, minY) != PointValues.Relation.CELL_OUTSIDE_QUERY || - lineRelateLine(x1, y1, x2, y2, maxX, minY, maxX, maxY) != PointValues.Relation.CELL_OUTSIDE_QUERY || - lineRelateLine(x1, y1, x2, y2, maxX, maxY, minX, maxY) != PointValues.Relation.CELL_OUTSIDE_QUERY || - lineRelateLine(x1, y1, x2, y2, minX, maxY, minX, minY) != PointValues.Relation.CELL_OUTSIDE_QUERY) { - return true; - } - - if (rightOffset > 0) { /* has left node */ - if (readLeft().crosses(minX, minY, maxX, maxY)) { - return true; - } - } - - if (rightOffset > 0 && maxY >= this.minY) { /* no right node if rightOffset == -1 */ - if (readRight().crosses(minX, minY, maxX, maxY)) { - return true; - } - } - } - return false; - } - - private static Edge readEdge(ByteBufferStreamInput input, int position) throws IOException { - input.position(position); - int minY = input.readInt(); - int maxY = input.readInt(); - int x1 = input.readInt(); - int y1 = input.readInt(); - int x2 = input.readInt(); - int y2 = input.readInt(); - int rightOffset = input.readInt(); - return new Edge(input, input.position(), x1, y1, x2, y2, minY, maxY, rightOffset); - } - } -} diff --git a/server/src/main/java/org/elasticsearch/index/mapper/GeoShapeFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/GeoShapeFieldMapper.java index 72b3e68fa025e..58b7322d0b9a1 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/GeoShapeFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/GeoShapeFieldMapper.java @@ -108,7 +108,7 @@ public GeoShapeFieldType fieldType() { @Override public void parse(ParseContext context) throws IOException { try { - Object shape = context.parseExternalValue(Object.class); + Geometry shape = context.parseExternalValue(Geometry.class); if (shape == null) { ShapeBuilder shapeBuilder = ShapeParser.parse(context.parser(), this); if (shapeBuilder == null) { 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 35fc1f3531362..368354b100192 100644 --- a/server/src/test/java/org/elasticsearch/common/geo/EdgeTreeTests.java +++ b/server/src/test/java/org/elasticsearch/common/geo/EdgeTreeTests.java @@ -25,8 +25,8 @@ import org.elasticsearch.test.geo.RandomShapeGenerator; import org.locationtech.spatial4j.shape.Rectangle; -import java.io.EOFException; import java.io.IOException; +import java.util.Arrays; public class EdgeTreeTests extends ESTestCase { @@ -38,58 +38,40 @@ public void testRectangleShape() throws IOException { int maxY = randomIntBetween(minY + 10, 180); int[] x = new int[]{minX, maxX, maxX, minX, minX}; int[] y = new int[]{minY, minY, maxY, maxY, minY}; - LinearRingEdgeTreeWriter writer = new LinearRingEdgeTreeWriter(x, y); + EdgeTreeWriter writer = new EdgeTreeWriter(x, y); BytesRef bytes = writer.toBytesRef(); - LinearRingEdgeTreeReader reader = new LinearRingEdgeTreeReader(bytes); + EdgeTreeReader reader = new EdgeTreeReader(bytes); // box-query touches bottom-left corner - assertTrue(reader.crosses(minX - randomIntBetween(1, 180), minY - randomIntBetween(1, 180), minX, minY)); - assertTrue(reader.containedIn(minX - randomIntBetween(1, 180), minY - randomIntBetween(1, 180), minX, minY)); + assertTrue(reader.containedInOrCrosses(minX - randomIntBetween(1, 180), minY - randomIntBetween(1, 180), minX, minY)); // box-query touches bottom-right corner - assertTrue(reader.crosses(maxX, minY - randomIntBetween(1, 180), maxX + randomIntBetween(1, 180), minY)); - assertTrue(reader.containedIn(maxX, minY - randomIntBetween(1, 180), maxX + randomIntBetween(1, 180), minY)); + assertTrue(reader.containedInOrCrosses(maxX, minY - randomIntBetween(1, 180), maxX + randomIntBetween(1, 180), minY)); // box-query touches top-right corner - assertTrue(reader.crosses(maxX, maxY, maxX + randomIntBetween(1, 180), maxY + randomIntBetween(1, 180))); - assertTrue(reader.containedIn(maxX, maxY, maxX + randomIntBetween(1, 180), maxY + randomIntBetween(1, 180))); + assertTrue(reader.containedInOrCrosses(maxX, maxY, maxX + randomIntBetween(1, 180), maxY + randomIntBetween(1, 180))); // box-query touches top-left corner - assertTrue(reader.crosses(minX - randomIntBetween(1, 180), maxY, minX, maxY + randomIntBetween(1, 180))); - - /** TODO(talevy): COME ON! Right Ray Test! */ - //assertTrue(reader.containedIn(minX - randomIntBetween(1, 180), maxY, minX, maxY + randomIntBetween(1, 180))); - + assertTrue(reader.containedInOrCrosses(minX - randomIntBetween(1, 180), maxY, minX, maxY + randomIntBetween(1, 180))); // box-query fully-enclosed inside rectangle - assertTrue(reader.containedIn((3 * minX + maxX) / 4, (3 * minY + maxY) / 4, (3 * maxX + minX) / 4, (3 * maxY + minY) / 4)); - assertFalse(reader.crosses((3 * minX + maxX) / 4, (3 * minY + maxY) / 4, (3 * maxX + minX) / 4, (3 * maxY + minY) / 4)); - + assertTrue(reader.containedInOrCrosses((3 * minX + maxX) / 4, (3 * minY + maxY) / 4, (3 * maxX + minX) / 4, (3 * maxY + minY) / 4)); // box-query fully-contains poly - assertTrue(reader.containedIn(minX - randomIntBetween(1, 180), minY - randomIntBetween(1, 180), + assertTrue(reader.containedInOrCrosses(minX - randomIntBetween(1, 180), minY - randomIntBetween(1, 180), maxX + randomIntBetween(1, 180), maxY + randomIntBetween(1, 180))); - // box-query half-in-half-out-right - // assertFalse(reader.containedIn((3 * minX + maxX) / 4, (3 * minY + maxY) / 4, maxX + randomIntBetween(1, 1000), (3 * maxY + minY) / 4)); - assertTrue(reader.crosses((3 * minX + maxX) / 4, (3 * minY + maxY) / 4, maxX + randomIntBetween(1, 1000), (3 * maxY + minY) / 4)); + assertTrue(reader.containedInOrCrosses((3 * minX + maxX) / 4, (3 * minY + maxY) / 4, maxX + randomIntBetween(1, 1000), (3 * maxY + minY) / 4)); // box-query half-in-half-out-left - // assertTrue(reader.containedIn(minX - randomIntBetween(1, 1000), (3 * minY + maxY) / 4, (3 * maxX + minX) / 4, (3 * maxY + minY) / 4)); - assertTrue(reader.crosses(minX - randomIntBetween(1, 1000), (3 * minY + maxY) / 4, (3 * maxX + minX) / 4, (3 * maxY + minY) / 4)); + assertTrue(reader.containedInOrCrosses(minX - randomIntBetween(1, 1000), (3 * minY + maxY) / 4, (3 * maxX + minX) / 4, (3 * maxY + minY) / 4)); // box-query half-in-half-out-top - // assertTrue(reader.containedIn((3 * minX + maxX) / 4, (3 * minY + maxY) / 4, maxX + randomIntBetween(1, 1000), maxY + randomIntBetween(1, 1000))); - assertTrue(reader.crosses((3 * minX + maxX) / 4, (3 * minY + maxY) / 4, maxX + randomIntBetween(1, 1000), maxY + randomIntBetween(1, 1000))); + assertTrue(reader.containedInOrCrosses((3 * minX + maxX) / 4, (3 * minY + maxY) / 4, maxX + randomIntBetween(1, 1000), maxY + randomIntBetween(1, 1000))); // box-query half-in-half-out-bottom - // assertTrue(reader.containedIn((3 * minX + maxX) / 4, minY - randomIntBetween(1, 1000), maxX + randomIntBetween(1, 1000), (3 * maxY + minY) / 4)); - assertTrue(reader.crosses((3 * minX + maxX) / 4, minY - randomIntBetween(1, 1000), maxX + randomIntBetween(1, 1000), (3 * maxY + minY) / 4)); + assertTrue(reader.containedInOrCrosses((3 * minX + maxX) / 4, minY - randomIntBetween(1, 1000), maxX + randomIntBetween(1, 1000), (3 * maxY + minY) / 4)); // box-query outside to the right - assertFalse(reader.containedIn(maxX + randomIntBetween(1, 1000), minY, maxX + randomIntBetween(1001, 2000), maxY)); - assertFalse(reader.crosses(maxX + randomIntBetween(1, 1000), minY, maxX + randomIntBetween(1001, 2000), maxY)); + assertFalse(reader.containedInOrCrosses(maxX + randomIntBetween(1, 1000), minY, maxX + randomIntBetween(1001, 2000), maxY)); // box-query outside to the left - assertFalse(reader.containedIn(maxX - randomIntBetween(1001, 2000), minY, minX - randomIntBetween(1, 1000), maxY)); - assertFalse(reader.crosses(maxX - randomIntBetween(1001, 2000), minY, minX - randomIntBetween(1, 1000), maxY)); + assertFalse(reader.containedInOrCrosses(maxX - randomIntBetween(1001, 2000), minY, minX - randomIntBetween(1, 1000), maxY)); // box-query outside to the top - assertFalse(reader.containedIn(minX, maxY + randomIntBetween(1, 1000), maxX, maxY + randomIntBetween(1001, 2000))); - assertFalse(reader.crosses(minX, maxY + randomIntBetween(1, 1000), maxX, maxY + randomIntBetween(1001, 2000))); + assertFalse(reader.containedInOrCrosses(minX, maxY + randomIntBetween(1, 1000), maxX, maxY + randomIntBetween(1001, 2000))); // box-query outside to the bottom - assertFalse(reader.containedIn(minX, minY - randomIntBetween(1001, 2000), maxX, minY - randomIntBetween(1, 1000))); - assertFalse(reader.crosses(minX, minY - randomIntBetween(1001, 2000), maxX, minY - randomIntBetween(1, 1000))); + assertFalse(reader.containedInOrCrosses(minX, minY - randomIntBetween(1001, 2000), maxX, minY - randomIntBetween(1, 1000))); } } @@ -106,16 +88,19 @@ public void testSimplePolygon() throws IOException { int[] x = asIntArray(geo.getPolygon().getLons()); int[] y = asIntArray(geo.getPolygon().getLats()); - LinearRingEdgeTreeWriter writer = new LinearRingEdgeTreeWriter(x, y); - LinearRingEdgeTreeReader reader = new LinearRingEdgeTreeReader(writer.toBytesRef()); - // box contained within polygon - assertTrue(reader.containedIn((minXBox + maxXBox) / 2, (minYBox + maxYBox) / 2, maxXBox - 1, maxYBox - 1)); - // fully contained within box - assertTrue(reader.containedIn(minXBox, minYBox, maxXBox, maxYBox)); - // crosses - assertTrue(reader.containedIn(minXBox, minYBox, maxXBox, maxYBox - 1)); + EdgeTreeWriter writer = new EdgeTreeWriter(x, y); + EdgeTreeReader reader = new EdgeTreeReader(writer.toBytesRef()); + // polygon fully contained within box + assertTrue(reader.containedInOrCrosses(minXBox, minYBox, maxXBox, maxYBox)); + // containedInOrCrosses + if (maxYBox - 1 >= minYBox) { + assertTrue(reader.containedInOrCrosses(minXBox, minYBox, maxXBox, maxYBox - 1)); + } + if (maxXBox -1 >= minXBox) { + assertTrue(reader.containedInOrCrosses(minXBox, minYBox, maxXBox - 1, maxYBox)); + } // does not cross - assertFalse(reader.containedIn(maxXBox + 1, maxYBox + 1, maxXBox + 10, maxYBox + 10)); + assertFalse(reader.containedInOrCrosses(maxXBox + 1, maxYBox + 1, maxXBox + 10, maxYBox + 10)); } } @@ -124,16 +109,16 @@ public void testPacMan() throws Exception { int[] px = {0, 10, 10, 0, -8, -10, -8, 0, 10, 10, 0}; int[] py = {0, 5, 9, 10, 9, 0, -9, -10, -9, -5, 0}; - // candidate crosses cell + // candidate containedInOrCrosses cell int xMin = 2;//-5; int xMax = 11;//0.000001; int yMin = -1;//0; int yMax = 1;//5; // test cell crossing poly - LinearRingEdgeTreeWriter writer = new LinearRingEdgeTreeWriter(px, py); - LinearRingEdgeTreeReader reader = new LinearRingEdgeTreeReader(writer.toBytesRef()); - assertTrue(reader.containedIn(xMin, yMin, xMax, yMax)); + EdgeTreeWriter writer = new EdgeTreeWriter(px, py); + EdgeTreeReader reader = new EdgeTreeReader(writer.toBytesRef()); + assertTrue(reader.containsBottomLeft(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 new file mode 100644 index 0000000000000..e6902ee403398 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/common/geo/GeometryTreeTests.java @@ -0,0 +1,92 @@ +/* + * 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.apache.lucene.util.BytesRef; +import org.elasticsearch.geo.geometry.LinearRing; +import org.elasticsearch.geo.geometry.Polygon; +import org.elasticsearch.test.ESTestCase; + +import java.io.IOException; +import java.util.Collections; + +public class GeometryTreeTests extends ESTestCase { + + public void testRectangleShape() throws IOException { + for (int i = 0; i < 1000; i++) { + int minX = randomIntBetween(-180, 170); + int maxX = randomIntBetween(minX + 10, 180); + int minY = randomIntBetween(-90, 80); + int maxY = randomIntBetween(minY + 10, 90); + double[] x = new double[]{minX, maxX, maxX, minX, minX}; + double[] y = new double[]{minY, minY, maxY, maxY, minY}; + GeometryTreeWriter writer = new GeometryTreeWriter(new Polygon(new LinearRing(y, x), Collections.emptyList())); + BytesRef bytes = writer.toBytesRef(); + GeometryTreeReader reader = new GeometryTreeReader(bytes); + + // 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 + assertTrue(reader.containedInOrCrosses(maxX, minY - randomIntBetween(1, 180), maxX + randomIntBetween(1, 180), minY)); + // box-query touches top-right corner + assertTrue(reader.containedInOrCrosses(maxX, maxY, maxX + randomIntBetween(1, 180), maxY + randomIntBetween(1, 180))); + // box-query touches top-left corner + assertTrue(reader.containedInOrCrosses(minX - randomIntBetween(1, 180), maxY, minX, maxY + randomIntBetween(1, 180))); + // box-query fully-enclosed inside rectangle + assertTrue(reader.containedInOrCrosses((3 * minX + maxX) / 4, (3 * minY + maxY) / 4, (3 * maxX + minX) / 4, (3 * maxY + minY) / 4)); + // box-query fully-contains poly + assertTrue(reader.containedInOrCrosses(minX - randomIntBetween(1, 180), minY - randomIntBetween(1, 180), + maxX + randomIntBetween(1, 180), maxY + randomIntBetween(1, 180))); + // box-query half-in-half-out-right + assertTrue(reader.containedInOrCrosses((3 * minX + maxX) / 4, (3 * minY + maxY) / 4, maxX + randomIntBetween(1, 1000), (3 * maxY + minY) / 4)); + // box-query half-in-half-out-left + assertTrue(reader.containedInOrCrosses(minX - randomIntBetween(1, 1000), (3 * minY + maxY) / 4, (3 * maxX + minX) / 4, (3 * maxY + minY) / 4)); + // box-query half-in-half-out-top + assertTrue(reader.containedInOrCrosses((3 * minX + maxX) / 4, (3 * minY + maxY) / 4, maxX + randomIntBetween(1, 1000), maxY + randomIntBetween(1, 1000))); + // box-query half-in-half-out-bottom + assertTrue(reader.containedInOrCrosses((3 * minX + maxX) / 4, minY - randomIntBetween(1, 1000), maxX + randomIntBetween(1, 1000), (3 * maxY + minY) / 4)); + + // box-query outside to the right + assertFalse(reader.containedInOrCrosses(maxX + randomIntBetween(1, 1000), minY, maxX + randomIntBetween(1001, 2000), maxY)); + // box-query outside to the left + assertFalse(reader.containedInOrCrosses(maxX - randomIntBetween(1001, 2000), minY, minX - randomIntBetween(1, 1000), maxY)); + // box-query outside to the top + assertFalse(reader.containedInOrCrosses(minX, maxY + randomIntBetween(1, 1000), maxX, maxY + randomIntBetween(1001, 2000))); + // box-query outside to the bottom + assertFalse(reader.containedInOrCrosses(minX, minY - randomIntBetween(1001, 2000), maxX, minY - randomIntBetween(1, 1000))); + } + } + + public void testPacMan() throws Exception { + // pacman + double[] px = {0, 10, 10, 0, -8, -10, -8, 0, 10, 10, 0}; + double[] py = {0, 5, 9, 10, 9, 0, -9, -10, -9, -5, 0}; + + // candidate containedInOrCrosses cell + int xMin = 2;//-5; + int xMax = 11;//0.000001; + int yMin = -1;//0; + int yMax = 1;//5; + + // test cell crossing poly + GeometryTreeWriter writer = new GeometryTreeWriter(new Polygon(new LinearRing(py, px), Collections.emptyList())); + GeometryTreeReader reader = new GeometryTreeReader(writer.toBytesRef()); + assertTrue(reader.containedInOrCrosses(xMin, yMin, xMax, yMax)); + } +} From ab2fade87112f33c8c1fd7bff78473356e7b0bd8 Mon Sep 17 00:00:00 2001 From: Tal Levy Date: Tue, 9 Apr 2019 18:22:18 -0700 Subject: [PATCH 4/8] check style --- .../elasticsearch/common/geo/EdgeTreeReader.java | 4 ++-- .../common/geo/GeometryTreeWriter.java | 8 ++++---- .../elasticsearch/common/geo/EdgeTreeTests.java | 16 ++++++++++------ .../common/geo/GeometryTreeTests.java | 15 ++++++++++----- 4 files changed, 26 insertions(+), 17 deletions(-) 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 946760c915e4c..78439b10d14ef 100644 --- a/server/src/main/java/org/elasticsearch/common/geo/EdgeTreeReader.java +++ b/server/src/main/java/org/elasticsearch/common/geo/EdgeTreeReader.java @@ -21,7 +21,6 @@ import org.apache.lucene.index.PointValues; 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; @@ -115,7 +114,8 @@ private boolean containsBottomLeft(ByteBufferStreamInput input, Edge root, int m if (root.maxY >= 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 (lineRelateLine(root.x1, root.y1, root.x2, root.y2, minX, minY, Integer.MAX_VALUE, minY) != PointValues.Relation.CELL_OUTSIDE_QUERY) { + if (lineRelateLine(root.x1, root.y1, root.x2, root.y2,minX, minY, + Integer.MAX_VALUE, minY) != PointValues.Relation.CELL_OUTSIDE_QUERY) { res = true; } 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 a05c311e6ef9a..7810798b4bfd1 100644 --- a/server/src/main/java/org/elasticsearch/common/geo/GeometryTreeWriter.java +++ b/server/src/main/java/org/elasticsearch/common/geo/GeometryTreeWriter.java @@ -20,7 +20,6 @@ import org.apache.lucene.util.BytesRef; import org.elasticsearch.common.io.stream.BytesStreamOutput; -import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.geo.geometry.Circle; import org.elasticsearch.geo.geometry.Geometry; import org.elasticsearch.geo.geometry.GeometryCollection; @@ -37,7 +36,6 @@ import java.io.IOException; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; public class GeometryTreeWriter { @@ -108,8 +106,10 @@ public Void visit(MultiPolygon multiPolygon) { @Override public Void visit(Rectangle r) { - int[] lats = new int[] { (int) r.getMinLat(), (int) r.getMinLat(), (int) r.getMaxLat(), (int) r.getMaxLat(), (int) r.getMinLat()}; - int[] lons = new int[] { (int) r.getMinLon(), (int) r.getMaxLon(), (int) r.getMaxLon(), (int) r.getMinLon(), (int) r.getMinLon()}; + int[] lats = new int[] { (int) r.getMinLat(), (int) r.getMinLat(), (int) r.getMaxLat(), (int) r.getMaxLat(), + (int) r.getMinLat()}; + int[] lons = new int[] { (int) r.getMinLon(), (int) r.getMaxLon(), (int) r.getMaxLon(), (int) r.getMinLon(), + (int) r.getMinLon()}; shapeWriters.add(new EdgeTreeWriter(lons, lats)); return null; } 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 368354b100192..386c439c15527 100644 --- a/server/src/test/java/org/elasticsearch/common/geo/EdgeTreeTests.java +++ b/server/src/test/java/org/elasticsearch/common/geo/EdgeTreeTests.java @@ -26,7 +26,6 @@ import org.locationtech.spatial4j.shape.Rectangle; import java.io.IOException; -import java.util.Arrays; public class EdgeTreeTests extends ESTestCase { @@ -51,18 +50,23 @@ public void testRectangleShape() throws IOException { // box-query touches top-left corner assertTrue(reader.containedInOrCrosses(minX - randomIntBetween(1, 180), maxY, minX, maxY + randomIntBetween(1, 180))); // box-query fully-enclosed inside rectangle - assertTrue(reader.containedInOrCrosses((3 * minX + maxX) / 4, (3 * minY + maxY) / 4, (3 * maxX + minX) / 4, (3 * maxY + minY) / 4)); + assertTrue(reader.containedInOrCrosses((3 * minX + maxX) / 4, (3 * minY + maxY) / 4, (3 * maxX + minX) / 4, + (3 * maxY + minY) / 4)); // box-query fully-contains poly assertTrue(reader.containedInOrCrosses(minX - randomIntBetween(1, 180), minY - randomIntBetween(1, 180), maxX + randomIntBetween(1, 180), maxY + randomIntBetween(1, 180))); // box-query half-in-half-out-right - assertTrue(reader.containedInOrCrosses((3 * minX + maxX) / 4, (3 * minY + maxY) / 4, maxX + randomIntBetween(1, 1000), (3 * maxY + minY) / 4)); + assertTrue(reader.containedInOrCrosses((3 * minX + maxX) / 4, (3 * minY + maxY) / 4, maxX + randomIntBetween(1, 1000), + (3 * maxY + minY) / 4)); // box-query half-in-half-out-left - assertTrue(reader.containedInOrCrosses(minX - randomIntBetween(1, 1000), (3 * minY + maxY) / 4, (3 * maxX + minX) / 4, (3 * maxY + minY) / 4)); + assertTrue(reader.containedInOrCrosses(minX - randomIntBetween(1, 1000), (3 * minY + maxY) / 4, (3 * maxX + minX) / 4, + (3 * maxY + minY) / 4)); // box-query half-in-half-out-top - assertTrue(reader.containedInOrCrosses((3 * minX + maxX) / 4, (3 * minY + maxY) / 4, maxX + randomIntBetween(1, 1000), maxY + randomIntBetween(1, 1000))); + assertTrue(reader.containedInOrCrosses((3 * minX + maxX) / 4, (3 * minY + maxY) / 4, maxX + randomIntBetween(1, 1000), + maxY + randomIntBetween(1, 1000))); // box-query half-in-half-out-bottom - assertTrue(reader.containedInOrCrosses((3 * minX + maxX) / 4, minY - randomIntBetween(1, 1000), maxX + randomIntBetween(1, 1000), (3 * maxY + minY) / 4)); + assertTrue(reader.containedInOrCrosses((3 * minX + maxX) / 4, minY - randomIntBetween(1, 1000), + maxX + randomIntBetween(1, 1000), (3 * maxY + minY) / 4)); // box-query outside to the right assertFalse(reader.containedInOrCrosses(maxX + randomIntBetween(1, 1000), minY, maxX + randomIntBetween(1001, 2000), maxY)); 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 e6902ee403398..2d200e7f82e47 100644 --- a/server/src/test/java/org/elasticsearch/common/geo/GeometryTreeTests.java +++ b/server/src/test/java/org/elasticsearch/common/geo/GeometryTreeTests.java @@ -49,18 +49,23 @@ public void testRectangleShape() throws IOException { // box-query touches top-left corner assertTrue(reader.containedInOrCrosses(minX - randomIntBetween(1, 180), maxY, minX, maxY + randomIntBetween(1, 180))); // box-query fully-enclosed inside rectangle - assertTrue(reader.containedInOrCrosses((3 * minX + maxX) / 4, (3 * minY + maxY) / 4, (3 * maxX + minX) / 4, (3 * maxY + minY) / 4)); + assertTrue(reader.containedInOrCrosses((3 * minX + maxX) / 4, (3 * minY + maxY) / 4, (3 * maxX + minX) / 4, + (3 * maxY + minY) / 4)); // box-query fully-contains poly assertTrue(reader.containedInOrCrosses(minX - randomIntBetween(1, 180), minY - randomIntBetween(1, 180), maxX + randomIntBetween(1, 180), maxY + randomIntBetween(1, 180))); // box-query half-in-half-out-right - assertTrue(reader.containedInOrCrosses((3 * minX + maxX) / 4, (3 * minY + maxY) / 4, maxX + randomIntBetween(1, 1000), (3 * maxY + minY) / 4)); + assertTrue(reader.containedInOrCrosses((3 * minX + maxX) / 4, (3 * minY + maxY) / 4, maxX + randomIntBetween(1, 1000), + (3 * maxY + minY) / 4)); // box-query half-in-half-out-left - assertTrue(reader.containedInOrCrosses(minX - randomIntBetween(1, 1000), (3 * minY + maxY) / 4, (3 * maxX + minX) / 4, (3 * maxY + minY) / 4)); + assertTrue(reader.containedInOrCrosses(minX - randomIntBetween(1, 1000), (3 * minY + maxY) / 4, (3 * maxX + minX) / 4, + (3 * maxY + minY) / 4)); // box-query half-in-half-out-top - assertTrue(reader.containedInOrCrosses((3 * minX + maxX) / 4, (3 * minY + maxY) / 4, maxX + randomIntBetween(1, 1000), maxY + randomIntBetween(1, 1000))); + assertTrue(reader.containedInOrCrosses((3 * minX + maxX) / 4, (3 * minY + maxY) / 4, maxX + randomIntBetween(1, 1000), + maxY + randomIntBetween(1, 1000))); // box-query half-in-half-out-bottom - assertTrue(reader.containedInOrCrosses((3 * minX + maxX) / 4, minY - randomIntBetween(1, 1000), maxX + randomIntBetween(1, 1000), (3 * maxY + minY) / 4)); + assertTrue(reader.containedInOrCrosses((3 * minX + maxX) / 4, minY - randomIntBetween(1, 1000), + maxX + randomIntBetween(1, 1000), (3 * maxY + minY) / 4)); // box-query outside to the right assertFalse(reader.containedInOrCrosses(maxX + randomIntBetween(1, 1000), minY, maxX + randomIntBetween(1001, 2000), maxY)); From b3df27041f7b223d3c8a341dda72e53a73477bd3 Mon Sep 17 00:00:00 2001 From: Tal Levy Date: Wed, 10 Apr 2019 17:24:24 -0700 Subject: [PATCH 5/8] add extent to geometry-tree --- .../common/geo/EdgeTreeReader.java | 26 ++++++------------- .../common/geo/EdgeTreeWriter.java | 10 ++++--- .../common/geo/GeometryTreeReader.java | 17 ++++++++++-- .../common/geo/GeometryTreeWriter.java | 22 ++++++++++++++-- 4 files changed, 49 insertions(+), 26 deletions(-) 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 78439b10d14ef..298a758023c2b 100644 --- a/server/src/main/java/org/elasticsearch/common/geo/EdgeTreeReader.java +++ b/server/src/main/java/org/elasticsearch/common/geo/EdgeTreeReader.java @@ -40,11 +40,10 @@ public boolean containedInOrCrosses(int minX, int minY, int maxX, int maxY) thro 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[] extent = readExtent(input); - int thisMinX = extent[0]; - int thisMinY = extent[1]; - int thisMaxX = extent[2]; - int thisMaxY = extent[3]; + int thisMinX = input.readInt(); + int thisMinY = input.readInt(); + int thisMaxX = input.readInt(); + int thisMaxY = input.readInt(); if (thisMinY > maxY || thisMaxX < minX || thisMaxY < minY || thisMinX > maxX) { return false; // tree and bbox-query are disjoint @@ -59,11 +58,10 @@ boolean containsBottomLeft(int minX, int minY, int maxX, int maxY) throws IOExce 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[] extent = readExtent(input); - int thisMinX = extent[0]; - int thisMinY = extent[1]; - int thisMaxX = extent[2]; - int thisMaxY = extent[3]; + int thisMinX = input.readInt(); + int thisMinY = input.readInt(); + int thisMaxX = input.readInt(); + int thisMaxY = input.readInt(); if (thisMinY > maxY || thisMaxX < minX || thisMaxY < minY || thisMinX > maxX) { return false; // tree and bbox-query are disjoint @@ -76,14 +74,6 @@ public boolean crosses(int minX, int minY, int maxX, int maxY) throws IOExceptio return crosses(input, readRoot(input, input.position()), minX, minY, maxX, maxY); } - public int[] readExtent(ByteBufferStreamInput input) throws IOException { - int minX = input.readInt(); - int minY = input.readInt(); - int maxX = input.readInt(); - int maxY = input.readInt(); - return new int[] { minX, minY, maxX, maxY }; - } - public Edge readRoot(ByteBufferStreamInput input, int position) throws IOException { return readEdge(input, position); } 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 36df8e46cfa35..2b7d2a4118e12 100644 --- a/server/src/main/java/org/elasticsearch/common/geo/EdgeTreeWriter.java +++ b/server/src/main/java/org/elasticsearch/common/geo/EdgeTreeWriter.java @@ -32,13 +32,15 @@ public class EdgeTreeWriter { */ static final int EDGE_SIZE_IN_BYTES = 28; - int minX = Integer.MAX_VALUE; - int minY = Integer.MAX_VALUE; - int maxX = Integer.MIN_VALUE; - int maxY = Integer.MIN_VALUE; + int minX; + int minY; + int maxX; + int maxY; final Edge tree; public EdgeTreeWriter(int[] x, int[] y) { + minX = minY = Integer.MAX_VALUE; + maxX = maxY = Integer.MIN_VALUE; Edge edges[] = new Edge[y.length - 1]; for (int i = 1; i < y.length; i++) { int y1 = y[i-1]; 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 538ab6cacff79..fb19e83bbca3d 100644 --- a/server/src/main/java/org/elasticsearch/common/geo/GeometryTreeReader.java +++ b/server/src/main/java/org/elasticsearch/common/geo/GeometryTreeReader.java @@ -33,16 +33,29 @@ public GeometryTreeReader(BytesRef bytesRef) { this.bytesRef = bytesRef; } - public boolean containedInOrCrosses(int minX, int minY, int maxX, int maxY) throws IOException { + 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)); + 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 + } + + if (minLon <= thisMinLon && minLat <= thisMinLat && maxLon >= thisMaxLon && maxLat >= thisMaxLat) { + return true; // bbox-query fully contains tree's extent. + } + int numTrees = input.readVInt(); for (int i = 0; i < numTrees; i++) { ShapeType shapeType = input.readEnum(ShapeType.class); if (ShapeType.POLYGON.equals(shapeType)) { BytesRef treeRef = input.readBytesRef(); EdgeTreeReader reader = new EdgeTreeReader(treeRef); - if (reader.containedInOrCrosses(minX, minY, maxX, maxY)) { + if (reader.containedInOrCrosses(minLon, minLat, maxLon, maxLat)) { return true; } } 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 7810798b4bfd1..88efb73c71643 100644 --- a/server/src/main/java/org/elasticsearch/common/geo/GeometryTreeWriter.java +++ b/server/src/main/java/org/elasticsearch/common/geo/GeometryTreeWriter.java @@ -49,6 +49,10 @@ public GeometryTreeWriter(Geometry geometry) { public BytesRef toBytesRef() throws IOException { BytesStreamOutput output = new BytesStreamOutput(); + output.writeInt(builder.minLon); + output.writeInt(builder.minLat); + output.writeInt(builder.maxLon); + output.writeInt(builder.maxLat); output.writeVInt(builder.shapeWriters.size()); for (EdgeTreeWriter writer : builder.shapeWriters) { output.writeEnum(ShapeType.POLYGON); @@ -61,9 +65,23 @@ public BytesRef toBytesRef() throws IOException { class GeometryTreeBuilder implements GeometryVisitor { private List shapeWriters; + int minLat; + int maxLat; + int minLon; + int maxLon; GeometryTreeBuilder() { shapeWriters = new ArrayList<>(); + minLat = minLon = Integer.MAX_VALUE; + maxLat = maxLon = Integer.MIN_VALUE; + } + + 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); + shapeWriters.add(writer); } @Override @@ -92,7 +110,7 @@ public Void visit(MultiLine multiLine) { public Void visit(Polygon polygon) { // TODO (support holes) LinearRing outerShell = polygon.getPolygon(); - shapeWriters.add(new EdgeTreeWriter(asIntArray(outerShell.getLons()), asIntArray(outerShell.getLats()))); + addWriter(new EdgeTreeWriter(asIntArray(outerShell.getLons()), asIntArray(outerShell.getLats()))); return null; } @@ -110,7 +128,7 @@ public Void visit(Rectangle r) { (int) r.getMinLat()}; int[] lons = new int[] { (int) r.getMinLon(), (int) r.getMaxLon(), (int) r.getMaxLon(), (int) r.getMinLon(), (int) r.getMinLon()}; - shapeWriters.add(new EdgeTreeWriter(lons, lats)); + addWriter(new EdgeTreeWriter(lons, lats)); return null; } From 9513b0a512bc1b0c43a158aeb01136fd30f0adfc Mon Sep 17 00:00:00 2001 From: Tal Levy Date: Thu, 11 Apr 2019 07:35:45 -0700 Subject: [PATCH 6/8] remove geometry-tree and add some comments --- .../common/geo/EdgeTreeReader.java | 23 ++- .../common/geo/EdgeTreeWriter.java | 8 + .../common/geo/GeometryTreeReader.java | 65 ------- .../common/geo/GeometryTreeWriter.java | 165 ------------------ .../common/geo/GeometryTreeTests.java | 97 ---------- 5 files changed, 30 insertions(+), 328 deletions(-) delete mode 100644 server/src/main/java/org/elasticsearch/common/geo/GeometryTreeReader.java delete mode 100644 server/src/main/java/org/elasticsearch/common/geo/GeometryTreeWriter.java delete mode 100644 server/src/test/java/org/elasticsearch/common/geo/GeometryTreeTests.java 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 298a758023c2b..4b25e150e9ca8 100644 --- a/server/src/main/java/org/elasticsearch/common/geo/EdgeTreeReader.java +++ b/server/src/main/java/org/elasticsearch/common/geo/EdgeTreeReader.java @@ -34,6 +34,9 @@ public EdgeTreeReader(BytesRef bytesRef) { this.bytesRef = bytesRef; } + /** + * 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); } @@ -99,6 +102,10 @@ Edge readRight(ByteBufferStreamInput input, Edge root) throws IOException { return readEdge(input, 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 { boolean res = false; if (root.maxY >= minY) { @@ -120,7 +127,9 @@ private boolean containsBottomLeft(ByteBufferStreamInput input, Edge root, int m return res; } - /** Returns true if the box crosses any edge in this edge subtree */ + /** + * 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; // we just have to cross one edge to answer the question, so we descend the tree and return when we do. @@ -160,6 +169,18 @@ private static class Edge { int maxY; int rightOffset; + /** + * Object representing an edge node read from bytes + * + * @param streamOffset offset in byte-reference where edge terminates + * @param x1 x-coordinate of first point in segment + * @param y1 y-coordinate of first point in segment + * @param x2 x-coordinate of second point in segment + * @param y2 y-coordinate of second point in segment + * @param minY minimum y-coordinate in this edge-node's tree + * @param maxY maximum y-coordinate in this edge-node's tree + * @param rightOffset the start offset in the byte-reference of the right edge-node + */ Edge(int streamOffset, int x1, int y1, int x2, int y2, int minY, int maxY, int rightOffset) { this.streamOffset = streamOffset; this.x1 = x1; 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 2b7d2a4118e12..4ffba80a12817 100644 --- a/server/src/main/java/org/elasticsearch/common/geo/EdgeTreeWriter.java +++ b/server/src/main/java/org/elasticsearch/common/geo/EdgeTreeWriter.java @@ -25,6 +25,9 @@ import java.io.IOException; import java.util.Arrays; +/** + * Shape edge-tree writer for use in doc-values + */ public class EdgeTreeWriter { /** @@ -67,10 +70,12 @@ public EdgeTreeWriter(int[] x, int[] y) { public BytesRef toBytesRef() throws IOException { BytesStreamOutput output = new BytesStreamOutput(4 * 4 + EDGE_SIZE_IN_BYTES * tree.size); + // write extent of edges output.writeInt(minX); output.writeInt(minY); output.writeInt(maxX); output.writeInt(maxY); + // write edge-tree itself writeTree(tree, output); output.close(); return output.bytes().toBytesRef(); @@ -122,6 +127,9 @@ private static Edge createTree(Edge edges[], int low, int high) { return newNode; } + /** + * Object representing an in-memory edge-tree to be serialized + */ static class Edge implements Comparable { final int x1; final int y1; diff --git a/server/src/main/java/org/elasticsearch/common/geo/GeometryTreeReader.java b/server/src/main/java/org/elasticsearch/common/geo/GeometryTreeReader.java deleted file mode 100644 index fb19e83bbca3d..0000000000000 --- a/server/src/main/java/org/elasticsearch/common/geo/GeometryTreeReader.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * 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.apache.lucene.util.BytesRef; -import org.elasticsearch.common.io.stream.ByteBufferStreamInput; -import org.elasticsearch.geo.geometry.ShapeType; - -import java.io.IOException; -import java.nio.ByteBuffer; - -public class GeometryTreeReader { - - private final BytesRef bytesRef; - - public GeometryTreeReader(BytesRef bytesRef) { - this.bytesRef = bytesRef; - } - - 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)); - 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 - } - - if (minLon <= thisMinLon && minLat <= thisMinLat && maxLon >= thisMaxLon && maxLat >= thisMaxLat) { - return true; // bbox-query fully contains tree's extent. - } - - int numTrees = input.readVInt(); - for (int i = 0; i < numTrees; i++) { - ShapeType shapeType = input.readEnum(ShapeType.class); - if (ShapeType.POLYGON.equals(shapeType)) { - BytesRef treeRef = input.readBytesRef(); - EdgeTreeReader reader = new EdgeTreeReader(treeRef); - if (reader.containedInOrCrosses(minLon, minLat, maxLon, maxLat)) { - return true; - } - } - } - return false; - } -} diff --git a/server/src/main/java/org/elasticsearch/common/geo/GeometryTreeWriter.java b/server/src/main/java/org/elasticsearch/common/geo/GeometryTreeWriter.java deleted file mode 100644 index 88efb73c71643..0000000000000 --- a/server/src/main/java/org/elasticsearch/common/geo/GeometryTreeWriter.java +++ /dev/null @@ -1,165 +0,0 @@ -/* - * 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.apache.lucene.util.BytesRef; -import org.elasticsearch.common.io.stream.BytesStreamOutput; -import org.elasticsearch.geo.geometry.Circle; -import org.elasticsearch.geo.geometry.Geometry; -import org.elasticsearch.geo.geometry.GeometryCollection; -import org.elasticsearch.geo.geometry.GeometryVisitor; -import org.elasticsearch.geo.geometry.Line; -import org.elasticsearch.geo.geometry.LinearRing; -import org.elasticsearch.geo.geometry.MultiLine; -import org.elasticsearch.geo.geometry.MultiPoint; -import org.elasticsearch.geo.geometry.MultiPolygon; -import org.elasticsearch.geo.geometry.Point; -import org.elasticsearch.geo.geometry.Polygon; -import org.elasticsearch.geo.geometry.Rectangle; -import org.elasticsearch.geo.geometry.ShapeType; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -public class GeometryTreeWriter { - - private final GeometryTreeBuilder builder; - - public GeometryTreeWriter(Geometry geometry) { - builder = new GeometryTreeBuilder(); - geometry.visit(builder); - } - - public BytesRef toBytesRef() throws IOException { - BytesStreamOutput output = new BytesStreamOutput(); - output.writeInt(builder.minLon); - output.writeInt(builder.minLat); - output.writeInt(builder.maxLon); - output.writeInt(builder.maxLat); - output.writeVInt(builder.shapeWriters.size()); - for (EdgeTreeWriter writer : builder.shapeWriters) { - output.writeEnum(ShapeType.POLYGON); - output.writeBytesRef(writer.toBytesRef()); - } - output.close(); - return output.bytes().toBytesRef(); - } - - class GeometryTreeBuilder implements GeometryVisitor { - - private List shapeWriters; - int minLat; - int maxLat; - int minLon; - int maxLon; - - GeometryTreeBuilder() { - shapeWriters = new ArrayList<>(); - minLat = minLon = Integer.MAX_VALUE; - maxLat = maxLon = Integer.MIN_VALUE; - } - - 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); - shapeWriters.add(writer); - } - - @Override - public Void visit(GeometryCollection collection) { - for (Geometry geometry : collection) { - geometry.visit(this); - } - return null; - } - - @Override - public Void visit(Line line) { - // TODO - return null; - } - - @Override - public Void visit(MultiLine multiLine) { - for (Line line : multiLine) { - visit(line); - } - return null; - } - - @Override - public Void visit(Polygon polygon) { - // TODO (support holes) - LinearRing outerShell = polygon.getPolygon(); - addWriter(new EdgeTreeWriter(asIntArray(outerShell.getLons()), asIntArray(outerShell.getLats()))); - return null; - } - - @Override - public Void visit(MultiPolygon multiPolygon) { - for (Polygon polygon : multiPolygon) { - visit(polygon); - } - return null; - } - - @Override - public Void visit(Rectangle r) { - int[] lats = new int[] { (int) r.getMinLat(), (int) r.getMinLat(), (int) r.getMaxLat(), (int) r.getMaxLat(), - (int) r.getMinLat()}; - int[] lons = new int[] { (int) r.getMinLon(), (int) r.getMaxLon(), (int) r.getMaxLon(), (int) r.getMinLon(), - (int) r.getMinLon()}; - addWriter(new EdgeTreeWriter(lons, lats)); - return null; - } - - @Override - public Void visit(Point point) { - // TODO - return null; - } - - @Override - public Void visit(MultiPoint multiPoint) { - // TODO - return null; - } - - @Override - public Void visit(LinearRing ring) { - throw new IllegalArgumentException("invalid shape type found [Circle]"); - } - - @Override - public Void visit(Circle circle) { - throw new IllegalArgumentException("invalid shape type found [Circle]"); - } - - private int[] asIntArray(double[] doub) { - int[] intArr = new int[doub.length]; - for (int i = 0; i < intArr.length; i++) { - intArr[i] = (int) doub[i]; - } - return intArr; - } - } -} diff --git a/server/src/test/java/org/elasticsearch/common/geo/GeometryTreeTests.java b/server/src/test/java/org/elasticsearch/common/geo/GeometryTreeTests.java deleted file mode 100644 index 2d200e7f82e47..0000000000000 --- a/server/src/test/java/org/elasticsearch/common/geo/GeometryTreeTests.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * 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.apache.lucene.util.BytesRef; -import org.elasticsearch.geo.geometry.LinearRing; -import org.elasticsearch.geo.geometry.Polygon; -import org.elasticsearch.test.ESTestCase; - -import java.io.IOException; -import java.util.Collections; - -public class GeometryTreeTests extends ESTestCase { - - public void testRectangleShape() throws IOException { - for (int i = 0; i < 1000; i++) { - int minX = randomIntBetween(-180, 170); - int maxX = randomIntBetween(minX + 10, 180); - int minY = randomIntBetween(-90, 80); - int maxY = randomIntBetween(minY + 10, 90); - double[] x = new double[]{minX, maxX, maxX, minX, minX}; - double[] y = new double[]{minY, minY, maxY, maxY, minY}; - GeometryTreeWriter writer = new GeometryTreeWriter(new Polygon(new LinearRing(y, x), Collections.emptyList())); - BytesRef bytes = writer.toBytesRef(); - GeometryTreeReader reader = new GeometryTreeReader(bytes); - - // 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 - assertTrue(reader.containedInOrCrosses(maxX, minY - randomIntBetween(1, 180), maxX + randomIntBetween(1, 180), minY)); - // box-query touches top-right corner - assertTrue(reader.containedInOrCrosses(maxX, maxY, maxX + randomIntBetween(1, 180), maxY + randomIntBetween(1, 180))); - // box-query touches top-left corner - assertTrue(reader.containedInOrCrosses(minX - randomIntBetween(1, 180), maxY, minX, maxY + randomIntBetween(1, 180))); - // box-query fully-enclosed inside rectangle - assertTrue(reader.containedInOrCrosses((3 * minX + maxX) / 4, (3 * minY + maxY) / 4, (3 * maxX + minX) / 4, - (3 * maxY + minY) / 4)); - // box-query fully-contains poly - assertTrue(reader.containedInOrCrosses(minX - randomIntBetween(1, 180), minY - randomIntBetween(1, 180), - maxX + randomIntBetween(1, 180), maxY + randomIntBetween(1, 180))); - // box-query half-in-half-out-right - assertTrue(reader.containedInOrCrosses((3 * minX + maxX) / 4, (3 * minY + maxY) / 4, maxX + randomIntBetween(1, 1000), - (3 * maxY + minY) / 4)); - // box-query half-in-half-out-left - assertTrue(reader.containedInOrCrosses(minX - randomIntBetween(1, 1000), (3 * minY + maxY) / 4, (3 * maxX + minX) / 4, - (3 * maxY + minY) / 4)); - // box-query half-in-half-out-top - assertTrue(reader.containedInOrCrosses((3 * minX + maxX) / 4, (3 * minY + maxY) / 4, maxX + randomIntBetween(1, 1000), - maxY + randomIntBetween(1, 1000))); - // box-query half-in-half-out-bottom - assertTrue(reader.containedInOrCrosses((3 * minX + maxX) / 4, minY - randomIntBetween(1, 1000), - maxX + randomIntBetween(1, 1000), (3 * maxY + minY) / 4)); - - // box-query outside to the right - assertFalse(reader.containedInOrCrosses(maxX + randomIntBetween(1, 1000), minY, maxX + randomIntBetween(1001, 2000), maxY)); - // box-query outside to the left - assertFalse(reader.containedInOrCrosses(maxX - randomIntBetween(1001, 2000), minY, minX - randomIntBetween(1, 1000), maxY)); - // box-query outside to the top - assertFalse(reader.containedInOrCrosses(minX, maxY + randomIntBetween(1, 1000), maxX, maxY + randomIntBetween(1001, 2000))); - // box-query outside to the bottom - assertFalse(reader.containedInOrCrosses(minX, minY - randomIntBetween(1001, 2000), maxX, minY - randomIntBetween(1, 1000))); - } - } - - public void testPacMan() throws Exception { - // pacman - double[] px = {0, 10, 10, 0, -8, -10, -8, 0, 10, 10, 0}; - double[] py = {0, 5, 9, 10, 9, 0, -9, -10, -9, -5, 0}; - - // candidate containedInOrCrosses cell - int xMin = 2;//-5; - int xMax = 11;//0.000001; - int yMin = -1;//0; - int yMax = 1;//5; - - // test cell crossing poly - GeometryTreeWriter writer = new GeometryTreeWriter(new Polygon(new LinearRing(py, px), Collections.emptyList())); - GeometryTreeReader reader = new GeometryTreeReader(writer.toBytesRef()); - assertTrue(reader.containedInOrCrosses(xMin, yMin, xMax, yMax)); - } -} From e615a1c50beb1f477f833fd2dae5c72634ef6779 Mon Sep 17 00:00:00 2001 From: Tal Levy Date: Fri, 12 Apr 2019 08:10:09 -0700 Subject: [PATCH 7/8] revert mapper change --- .../org/elasticsearch/index/mapper/GeoShapeFieldMapper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/org/elasticsearch/index/mapper/GeoShapeFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/GeoShapeFieldMapper.java index 58b7322d0b9a1..72b3e68fa025e 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/GeoShapeFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/GeoShapeFieldMapper.java @@ -108,7 +108,7 @@ public GeoShapeFieldType fieldType() { @Override public void parse(ParseContext context) throws IOException { try { - Geometry shape = context.parseExternalValue(Geometry.class); + Object shape = context.parseExternalValue(Object.class); if (shape == null) { ShapeBuilder shapeBuilder = ShapeParser.parse(context.parser(), this); if (shapeBuilder == null) { From 9959c0973f07affb191a7a2aa5a9bd211d26c80b Mon Sep 17 00:00:00 2001 From: Tal Levy Date: Tue, 7 May 2019 12:16:17 -0700 Subject: [PATCH 8/8] update with latest lucene --- .../elasticsearch/common/geo/EdgeTreeReader.java | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) 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 4b25e150e9ca8..250e98e09d114 100644 --- a/server/src/main/java/org/elasticsearch/common/geo/EdgeTreeReader.java +++ b/server/src/main/java/org/elasticsearch/common/geo/EdgeTreeReader.java @@ -18,14 +18,13 @@ */ package org.elasticsearch.common.geo; -import org.apache.lucene.index.PointValues; import org.apache.lucene.util.BytesRef; import org.elasticsearch.common.io.stream.ByteBufferStreamInput; import java.io.IOException; import java.nio.ByteBuffer; -import static org.apache.lucene.geo.GeoUtils.lineRelateLine; +import static org.apache.lucene.geo.GeoUtils.lineCrossesLine; public class EdgeTreeReader { final BytesRef bytesRef; @@ -111,8 +110,7 @@ private boolean containsBottomLeft(ByteBufferStreamInput input, Edge root, int m if (root.maxY >= 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 (lineRelateLine(root.x1, root.y1, root.x2, root.y2,minX, minY, - Integer.MAX_VALUE, minY) != PointValues.Relation.CELL_OUTSIDE_QUERY) { + if (lineCrossesLine(root.x1, root.y1, root.x2, root.y2,minX, minY, Integer.MAX_VALUE, minY)) { res = true; } @@ -136,10 +134,10 @@ private boolean crosses(ByteBufferStreamInput input, Edge root, int minX, int mi if (root.maxY >= minY) { // does rectangle's edges intersect or reside inside polygon's edge - if (lineRelateLine(root.x1, root.y1, root.x2, root.y2, minX, minY, maxX, minY) != PointValues.Relation.CELL_OUTSIDE_QUERY || - lineRelateLine(root.x1, root.y1, root.x2, root.y2, maxX, minY, maxX, maxY) != PointValues.Relation.CELL_OUTSIDE_QUERY || - lineRelateLine(root.x1, root.y1, root.x2, root.y2, maxX, maxY, minX, maxY) != PointValues.Relation.CELL_OUTSIDE_QUERY || - lineRelateLine(root.x1, root.y1, root.x2, root.y2, minX, maxY, minX, minY) != PointValues.Relation.CELL_OUTSIDE_QUERY) { + if (lineCrossesLine(root.x1, root.y1, root.x2, root.y2, minX, minY, maxX, minY) || + lineCrossesLine(root.x1, root.y1, root.x2, root.y2, maxX, minY, maxX, maxY) || + lineCrossesLine(root.x1, root.y1, root.x2, root.y2, maxX, maxY, minX, maxY) || + lineCrossesLine(root.x1, root.y1, root.x2, root.y2, minX, maxY, minX, minY)) { return true; }