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 b1f1eb5a1a622..883235c661c70 100644 --- a/server/src/main/java/org/elasticsearch/common/geo/EdgeTreeReader.java +++ b/server/src/main/java/org/elasticsearch/common/geo/EdgeTreeReader.java @@ -26,7 +26,11 @@ import static org.apache.lucene.geo.GeoUtils.lineCrossesLineWithBoundary; -public class EdgeTreeReader { +/** + * This {@link ShapeTreeReader} understands how to parse polygons + * serialized with the {@link EdgeTreeWriter} + */ +public class EdgeTreeReader implements ShapeTreeReader { final ByteBufferStreamInput input; final int startPosition; @@ -43,8 +47,7 @@ public Extent getExtent() throws IOException { /** * 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 { - Extent extent = new Extent(minX, minY, maxX, maxY); + public boolean intersects(Extent extent) throws IOException { return this.containsBottomLeft(extent) || this.crosses(extent); } 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 82a124775ba7f..ccbc22dd57abe 100644 --- a/server/src/main/java/org/elasticsearch/common/geo/EdgeTreeWriter.java +++ b/server/src/main/java/org/elasticsearch/common/geo/EdgeTreeWriter.java @@ -20,6 +20,7 @@ import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.geo.geometry.ShapeType; import java.io.IOException; import java.util.Arrays; @@ -27,14 +28,14 @@ /** * Shape edge-tree writer for use in doc-values */ -public class EdgeTreeWriter implements Writeable { +public class EdgeTreeWriter extends ShapeTreeWriter { /** * | minY | maxY | x1 | y1 | x2 | y2 | right_offset | */ static final int EDGE_SIZE_IN_BYTES = 28; - Extent extent; + private final Extent extent; final Edge tree; public EdgeTreeWriter(int[] x, int[] y) { @@ -67,6 +68,16 @@ public EdgeTreeWriter(int[] x, int[] y) { this.tree = createTree(edges, 0, edges.length - 1); } + @Override + public Extent getExtent() { + return extent; + } + + @Override + public ShapeType getShapeType() { + return ShapeType.POLYGON; + } + @Override public void writeTo(StreamOutput out) throws IOException { //out.writeVInt(4 * 4 + EDGE_SIZE_IN_BYTES * tree.size); 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 206e3b428be21..2b5c12a9debd1 100644 --- a/server/src/main/java/org/elasticsearch/common/geo/GeometryTreeReader.java +++ b/server/src/main/java/org/elasticsearch/common/geo/GeometryTreeReader.java @@ -48,20 +48,15 @@ public Extent getExtent() throws IOException { } assert input.readVInt() == 1; ShapeType shapeType = input.readEnum(ShapeType.class); - if (ShapeType.POLYGON.equals(shapeType)) { - EdgeTreeReader reader = new EdgeTreeReader(input); - return reader.getExtent(); - } else { - throw new UnsupportedOperationException("only polygons supported -- TODO"); - } + ShapeTreeReader reader = getReader(shapeType, input); + return reader.getExtent(); } - public boolean containedInOrCrosses(int minLon, int minLat, int maxLon, int maxLat) throws IOException { + public boolean intersects(Extent extent) throws IOException { input.position(0); boolean hasExtent = input.readBoolean(); if (hasExtent) { - Optional extentCheck = EdgeTreeReader.checkExtent(input, - new Extent(minLon, minLat, maxLon, maxLat)); + Optional extentCheck = EdgeTreeReader.checkExtent(input, extent); if (extentCheck.isPresent()) { return extentCheck.get(); } @@ -70,13 +65,26 @@ public boolean containedInOrCrosses(int minLon, int minLat, int maxLon, int maxL int numTrees = input.readVInt(); for (int i = 0; i < numTrees; i++) { ShapeType shapeType = input.readEnum(ShapeType.class); - if (ShapeType.POLYGON.equals(shapeType)) { - EdgeTreeReader reader = new EdgeTreeReader(input); - if (reader.containedInOrCrosses(minLon, minLat, maxLon, maxLat)) { - return true; - } + ShapeTreeReader reader = getReader(shapeType, input); + if (reader.intersects(extent)) { + return true; } } return false; } + + private static ShapeTreeReader getReader(ShapeType shapeType, ByteBufferStreamInput input) throws IOException { + switch (shapeType) { + case POLYGON: + return new EdgeTreeReader(input); + case POINT: + case MULTIPOINT: + return new Point2DReader(input); + case LINESTRING: + case MULTILINESTRING: + throw new UnsupportedOperationException("TODO: linestring and multilinestring"); + default: + throw new UnsupportedOperationException("unsupported shape type [" + shapeType + "]"); + } + } } 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 35b85d359bf14..32318595edfd9 100644 --- a/server/src/main/java/org/elasticsearch/common/geo/GeometryTreeWriter.java +++ b/server/src/main/java/org/elasticsearch/common/geo/GeometryTreeWriter.java @@ -32,7 +32,6 @@ 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; @@ -65,15 +64,15 @@ public void writeTo(StreamOutput out) throws IOException { out.writeInt(builder.maxLat); } out.writeVInt(builder.shapeWriters.size()); - for (EdgeTreeWriter writer : builder.shapeWriters) { - out.writeEnum(ShapeType.POLYGON); + for (ShapeTreeWriter writer : builder.shapeWriters) { + out.writeEnum(writer.getShapeType()); writer.writeTo(out); } } class GeometryTreeBuilder implements GeometryVisitor { - private List shapeWriters; + private List shapeWriters; // integers are used to represent int-encoded lat/lon values int minLat; int maxLat; @@ -86,11 +85,12 @@ class GeometryTreeBuilder implements GeometryVisitor { maxLat = maxLon = Integer.MIN_VALUE; } - private void addWriter(EdgeTreeWriter writer) { - minLon = Math.min(minLon, writer.extent.minX); - minLat = Math.min(minLat, writer.extent.minY); - maxLon = Math.max(maxLon, writer.extent.maxX); - maxLat = Math.max(maxLat, writer.extent.maxY); + private void addWriter(ShapeTreeWriter writer) { + Extent extent = writer.getExtent(); + minLon = Math.min(minLon, extent.minX); + minLat = Math.min(minLat, extent.minY); + maxLon = Math.max(maxLon, extent.maxX); + maxLat = Math.max(maxLat, extent.maxY); shapeWriters.add(writer); } @@ -143,12 +143,16 @@ public Void visit(Rectangle r) { @Override public Void visit(Point point) { - throw new UnsupportedOperationException("support for Point is a TODO"); + Point2DWriter writer = new Point2DWriter(point); + addWriter(writer); + return null; } @Override public Void visit(MultiPoint multiPoint) { - throw new UnsupportedOperationException("support for MultiPoint is a TODO"); + Point2DWriter writer = new Point2DWriter(multiPoint); + addWriter(writer); + return null; } @Override diff --git a/server/src/main/java/org/elasticsearch/common/geo/Point2DReader.java b/server/src/main/java/org/elasticsearch/common/geo/Point2DReader.java new file mode 100644 index 0000000000000..2db7f27025116 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/common/geo/Point2DReader.java @@ -0,0 +1,104 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.elasticsearch.common.geo; + +import org.elasticsearch.common.io.stream.ByteBufferStreamInput; + +import java.io.IOException; +import java.util.ArrayDeque; +import java.util.Deque; + +/** + * This {@link ShapeTreeReader} understands how to parse points + * serialized with the {@link Point2DWriter} + */ +class Point2DReader implements ShapeTreeReader { + private final ByteBufferStreamInput input; + private final int size; + private final int startPosition; + + Point2DReader(ByteBufferStreamInput input) throws IOException { + this.input = input; + this.size = input.readVInt(); + this.startPosition = input.position(); + } + + public Extent getExtent() throws IOException { + if (size == 2) { + int x = readX(0); + int y = readY(0); + return new Extent(x, y, x, y); + } else { + return new Extent(input); + } + } + + public boolean intersects(Extent extent) throws IOException { + Deque stack = new ArrayDeque<>(); + stack.push(0); + stack.push(size - 1); + stack.push(0); + while (stack.isEmpty() == false) { + int axis = stack.pop(); + int right = stack.pop(); + int left = stack.pop(); + + if (right - left <= Point2DWriter.LEAF_SIZE) { + for (int i = left; i <= right; i++) { + // TODO serialize to re-usable array instead of serializing in each step + int x = readX(i); + int y = readY(i); + if (x >= extent.minX && x <= extent.maxX && y >= extent.minY && y <= extent.maxY) { + return true; + } + } + continue; + } + + int middle = (right - left) >> 1; + int x = readX(middle); + int y = readY(middle); + if (x >= extent.minX && x <= extent.maxX && y >= extent.minY && y <= extent.maxY) { + return true; + } + if ((axis == 0 && extent.minX <= x) || (axis == 1 && extent.minY <= y)) { + stack.push(left); + stack.push(middle - 1); + stack.push(1 - axis); + } + if ((axis == 0 && extent.maxX >= x) || (axis == 1 && extent.maxY >= y)) { + stack.push(middle + 1); + stack.push(right); + stack.push(1 - axis); + } + } + + return false; + } + + private int readX(int pointIdx) throws IOException { + input.position(startPosition + 2 * pointIdx * Integer.BYTES); + return input.readInt(); + } + + private int readY(int pointIdx) throws IOException { + input.position(startPosition + (2 * pointIdx + 1) * Integer.BYTES); + return input.readInt(); + } +} diff --git a/server/src/main/java/org/elasticsearch/common/geo/Point2DWriter.java b/server/src/main/java/org/elasticsearch/common/geo/Point2DWriter.java new file mode 100644 index 0000000000000..d993a19dd4ea7 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/common/geo/Point2DWriter.java @@ -0,0 +1,178 @@ +/* + * 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.geo.GeoEncodingUtils; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.geo.geometry.MultiPoint; +import org.elasticsearch.geo.geometry.Point; +import org.elasticsearch.geo.geometry.ShapeType; + +import java.io.IOException; + +/** + * points KD-Tree (2D) writer for use in doc-values. + * + * This work is influenced by https://github.com/mourner/kdbush (ISC licensed). + */ +public class Point2DWriter extends ShapeTreeWriter { + + private static final int K = 2; + private final Extent extent; + private final int[] coords; + // size of a leaf node where searches are done sequentially. + static final int LEAF_SIZE = 64; + + Point2DWriter(MultiPoint multiPoint) { + int numPoints = multiPoint.size(); + int minX = Integer.MAX_VALUE; + int minY = Integer.MAX_VALUE; + int maxX = Integer.MIN_VALUE; + int maxY = Integer.MIN_VALUE; + coords = new int[numPoints * K]; + int i = 0; + for (Point point : multiPoint) { + int x = GeoEncodingUtils.encodeLongitude(point.getLon()); + int y = GeoEncodingUtils.encodeLatitude(point.getLat()); + minX = Math.min(minX, x); + minY = Math.min(minY, y); + maxX = Math.max(maxX, x); + maxY = Math.max(maxY, y); + coords[2 * i] = x; + coords[2 * i + 1] = y; + i++; + } + sort(0, numPoints - 1, 0); + this.extent = new Extent(minX, minY, maxX, maxY); + } + + Point2DWriter(Point point) { + int x = GeoEncodingUtils.encodeLongitude(point.getLon()); + int y = GeoEncodingUtils.encodeLatitude(point.getLat()); + coords = new int[] {x, y}; + this.extent = new Extent(x, y, x, y); + } + + @Override + public Extent getExtent() { + return extent; + } + + @Override + public ShapeType getShapeType() { + return ShapeType.MULTIPOINT; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + int numPoints = coords.length >> 1; + out.writeVInt(numPoints); + if (numPoints > 1) { + extent.writeTo(out); + } + for (int coord : coords) { + out.writeInt(coord); + } + } + + private void sort(int left, int right, int depth) { + // since the reader will search through points within a leaf, + // there is no improved performance by sorting these points. + if (right - left <= LEAF_SIZE) { + return; + } + + int middle = (left + right) >> 1; + + select(left, right, middle, depth); + + sort(left, middle - 1, depth + 1); + sort(middle + 1, right, depth + 1); + } + + /** + * A slightly-modified Floyd-Rivest selection algorithm, + * https://en.wikipedia.org/wiki/Floyd%E2%80%93Rivest_algorithm + * + * @param left the index of the left point + * @param right the index of the right point + * @param k the pivot index + * @param depth the depth in the kd-tree + */ + private void select(int left, int right, int k, int depth) { + int axis = depth % K; + while (right > left) { + if (right - left > 600) { + double n = right - left + 1; + int i = k - left + 1; + double z = Math.log(n); + double s = 0.5 * Math.exp(2 * z / 3); + double sd = 0.5 * Math.sqrt(z * s * (n - s) / n) * ((i - n / 2) < 0 ? -1 : 1); + int newLeft = Math.max(left, (int) Math.floor(k - i * s / n + sd)); + int newRight = Math.min(right, (int) Math.floor(k + (n - i) * s / n + sd)); + select(newLeft, newRight, k, depth); + } + int t = coords[2 * k + axis]; + int i = left; + int j = right; + + swapPoint(i, j); + if (coords[2 * right + axis] > t) { + swapPoint(left, right); + } + + while (i < j) { + swapPoint(i, j); + i++; + j--; + while (coords[2 * i + axis] < t) { + i++; + } + while (coords[2 * j + axis] > t) { + j--; + } + } + + if (coords[2 * left + axis] == t) { + swapPoint(left, j); + } else { + j++; + swapPoint(j, right); + } + + if (j <= k) { + left = j + 1; + } + if (k <= j) { + right = j - 1; + } + } + } + + private void swapPoint(int i, int j) { + swap( 2 * i, 2 * j); + swap(2 * i + 1, 2 * j + 1); + } + + private void swap(int i, int j) { + int tmp = coords[i]; + coords[i] = coords[j]; + coords[j] = tmp; + } +} diff --git a/server/src/main/java/org/elasticsearch/common/geo/ShapeTreeReader.java b/server/src/main/java/org/elasticsearch/common/geo/ShapeTreeReader.java new file mode 100644 index 0000000000000..6931007e76344 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/common/geo/ShapeTreeReader.java @@ -0,0 +1,32 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.elasticsearch.common.geo; + +import org.elasticsearch.geo.geometry.Geometry; + +import java.io.IOException; + +/** + * Shape Reader to read different {@link Geometry} doc-values + */ +public interface ShapeTreeReader { + + Extent getExtent() throws IOException; + boolean intersects(Extent extent) throws IOException; +} diff --git a/server/src/main/java/org/elasticsearch/common/geo/ShapeTreeWriter.java b/server/src/main/java/org/elasticsearch/common/geo/ShapeTreeWriter.java new file mode 100644 index 0000000000000..2c16377a9f719 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/common/geo/ShapeTreeWriter.java @@ -0,0 +1,32 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.elasticsearch.common.geo; + +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.geo.geometry.ShapeType; + +/** + * Shape writer for use in doc-values + */ +public abstract class ShapeTreeWriter implements Writeable { + + public abstract Extent getExtent(); + + public abstract ShapeType getShapeType(); +} 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 eb0e2603b57e9..abd94ae333fa6 100644 --- a/server/src/test/java/org/elasticsearch/common/geo/EdgeTreeTests.java +++ b/server/src/test/java/org/elasticsearch/common/geo/EdgeTreeTests.java @@ -50,40 +50,40 @@ public void testRectangleShape() throws IOException { EdgeTreeReader reader = new EdgeTreeReader(new ByteBufferStreamInput(ByteBuffer.wrap(output.bytes().toBytesRef().bytes))); // box-query touches bottom-left corner - assertTrue(reader.containedInOrCrosses(minX - randomIntBetween(1, 180), minY - randomIntBetween(1, 180), minX, minY)); + assertTrue(reader.intersects(new Extent(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)); + assertTrue(reader.intersects(new Extent(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))); + assertTrue(reader.intersects(new Extent(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))); + assertTrue(reader.intersects(new Extent(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.intersects(new Extent((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))); + assertTrue(reader.intersects(new Extent(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.intersects(new Extent((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.intersects(new Extent(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.intersects(new Extent((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.intersects(new Extent((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)); + assertFalse(reader.intersects(new Extent(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)); + assertFalse(reader.intersects(new Extent(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))); + assertFalse(reader.intersects(new Extent(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))); + assertFalse(reader.intersects(new Extent(minX, minY - randomIntBetween(1001, 2000), maxX, minY - randomIntBetween(1, 1000)))); } } @@ -107,16 +107,16 @@ public void testSimplePolygon() throws IOException { EdgeTreeReader reader = new EdgeTreeReader(new ByteBufferStreamInput(ByteBuffer.wrap(output.bytes().toBytesRef().bytes))); assertThat(reader.getExtent(), equalTo(new Extent(minXBox, minYBox, maxXBox, maxYBox))); // polygon fully contained within box - assertTrue(reader.containedInOrCrosses(minXBox, minYBox, maxXBox, maxYBox)); - // containedInOrCrosses + assertTrue(reader.intersects(new Extent(minXBox, minYBox, maxXBox, maxYBox))); + // intersects if (maxYBox - 1 >= minYBox) { - assertTrue(reader.containedInOrCrosses(minXBox, minYBox, maxXBox, maxYBox - 1)); + assertTrue(reader.intersects(new Extent(minXBox, minYBox, maxXBox, maxYBox - 1))); } if (maxXBox -1 >= minXBox) { - assertTrue(reader.containedInOrCrosses(minXBox, minYBox, maxXBox - 1, maxYBox)); + assertTrue(reader.intersects(new Extent(minXBox, minYBox, maxXBox - 1, maxYBox))); } // does not cross - assertFalse(reader.containedInOrCrosses(maxXBox + 1, maxYBox + 1, maxXBox + 10, maxYBox + 10)); + assertFalse(reader.intersects(new Extent(maxXBox + 1, maxYBox + 1, maxXBox + 10, maxYBox + 10))); } } @@ -125,7 +125,7 @@ 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 containedInOrCrosses cell + // candidate intersects cell int xMin = 2;//-5; int xMax = 11;//0.000001; int yMin = -1;//0; 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 0a33149c7e989..eafb836fb6586 100644 --- a/server/src/test/java/org/elasticsearch/common/geo/GeometryTreeTests.java +++ b/server/src/test/java/org/elasticsearch/common/geo/GeometryTreeTests.java @@ -20,11 +20,15 @@ import org.elasticsearch.common.io.stream.BytesStreamOutput; import org.elasticsearch.geo.geometry.LinearRing; +import org.elasticsearch.geo.geometry.MultiPoint; +import org.elasticsearch.geo.geometry.Point; import org.elasticsearch.geo.geometry.Polygon; import org.elasticsearch.test.ESTestCase; import java.io.IOException; +import java.util.Arrays; import java.util.Collections; +import java.util.List; import static org.hamcrest.Matchers.equalTo; @@ -48,40 +52,40 @@ public void testRectangleShape() throws IOException { assertThat(reader.getExtent(), equalTo(new Extent(minX, minY, maxX, maxY))); // box-query touches bottom-left corner - assertTrue(reader.containedInOrCrosses(minX - randomIntBetween(1, 180), minY - randomIntBetween(1, 180), minX, minY)); + assertTrue(reader.intersects(new Extent(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)); + assertTrue(reader.intersects(new Extent(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))); + assertTrue(reader.intersects(new Extent(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))); + assertTrue(reader.intersects(new Extent(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.intersects(new Extent((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))); + assertTrue(reader.intersects(new Extent(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.intersects(new Extent((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.intersects(new Extent(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.intersects(new Extent((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.intersects(new Extent((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)); + assertFalse(reader.intersects(new Extent(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)); + assertFalse(reader.intersects(new Extent(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))); + assertFalse(reader.intersects(new Extent(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))); + assertFalse(reader.intersects(new Extent(minX, minY - randomIntBetween(1001, 2000), maxX, minY - randomIntBetween(1, 1000)))); } } @@ -90,7 +94,7 @@ public void testPacMan() throws Exception { 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 + // candidate intersects cell int xMin = 2;//-5; int xMax = 11;//0.000001; int yMin = -1;//0; @@ -102,6 +106,37 @@ public void testPacMan() throws Exception { writer.writeTo(output); output.close(); GeometryTreeReader reader = new GeometryTreeReader(output.bytes().toBytesRef()); - assertTrue(reader.containedInOrCrosses(xMin, yMin, xMax, yMax)); + assertTrue(reader.intersects(new Extent(xMin, yMin, xMax, yMax))); + } + + public void testPacManPoints() throws Exception { + // pacman + List points = Arrays.asList( + new Point(0, 0), + new Point(5, 10), + new Point(9, 10), + new Point(10, 0), + new Point(9, -8), + new Point(0, -10), + new Point(-9, -8), + new Point(-10, 0), + new Point(-9, 10), + new Point(-5, 10) + ); + + + // candidate intersects cell + int xMin = 0; + int xMax = 11; + int yMin = -10; + int yMax = 9; + + // test cell crossing poly + GeometryTreeWriter writer = new GeometryTreeWriter(new MultiPoint(points)); + BytesStreamOutput output = new BytesStreamOutput(); + writer.writeTo(output); + output.close(); + GeometryTreeReader reader = new GeometryTreeReader(output.bytes().toBytesRef()); + assertTrue(reader.intersects(new Extent(xMin, yMin, xMax, yMax))); } } diff --git a/server/src/test/java/org/elasticsearch/common/geo/Point2DTests.java b/server/src/test/java/org/elasticsearch/common/geo/Point2DTests.java new file mode 100644 index 0000000000000..1a98704eff821 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/common/geo/Point2DTests.java @@ -0,0 +1,80 @@ +/* + * 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.geo.GeoEncodingUtils; +import org.elasticsearch.common.geo.builders.PointBuilder; +import org.elasticsearch.common.io.stream.ByteBufferStreamInput; +import org.elasticsearch.common.io.stream.BytesStreamOutput; +import org.elasticsearch.geo.geometry.MultiPoint; +import org.elasticsearch.geo.geometry.Point; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.geo.RandomShapeGenerator; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +public class Point2DTests extends ESTestCase { + + public void testOnePoint() throws IOException { + PointBuilder gen = (PointBuilder) RandomShapeGenerator.createShape(random(), RandomShapeGenerator.ShapeType.POINT); + Point point = gen.buildGeometry(); + int x = GeoEncodingUtils.encodeLongitude(point.getLon()); + int y = GeoEncodingUtils.encodeLatitude(point.getLat()); + Point2DWriter writer = new Point2DWriter(point); + + BytesStreamOutput output = new BytesStreamOutput(); + writer.writeTo(output); + output.close(); + Point2DReader reader = new Point2DReader(new ByteBufferStreamInput(ByteBuffer.wrap(output.bytes().toBytesRef().bytes))); + assertTrue(reader.intersects(new Extent(x, y, x, y))); + assertTrue(reader.intersects(new Extent(x, y, x + randomIntBetween(1, 10), y + randomIntBetween(1, 10)))); + assertTrue(reader.intersects(new Extent(x - randomIntBetween(1, 10), y - randomIntBetween(1, 10), x, y))); + assertTrue(reader.intersects(new Extent(x - randomIntBetween(1, 10), y - randomIntBetween(1, 10), + x + randomIntBetween(1, 10), y + randomIntBetween(1, 10)))); + assertFalse(reader.intersects(new Extent(x - randomIntBetween(10, 100), y - randomIntBetween(10, 100), + x - randomIntBetween(1, 10), y - randomIntBetween(1, 10)))); + } + + public void testPoints() throws IOException { + for (int i = 0; i < 100; i++) { + int minX = randomIntBetween(-180, 170); + int maxX = randomIntBetween(minX + 10, 180); + int minY = randomIntBetween(-90, 80); + int maxY = randomIntBetween(minY + 10, 90); + Extent extent = new Extent(GeoEncodingUtils.encodeLongitude(minX), GeoEncodingUtils.encodeLatitude(minY), + GeoEncodingUtils.encodeLongitude(maxX), GeoEncodingUtils.encodeLatitude(maxY)); + int numPoints = randomIntBetween(2, 1000); + + List points = new ArrayList<>(numPoints); + for (int j = 0; j < numPoints; j++) { + points.add(new Point(randomDoubleBetween(minY, maxY, true), randomDoubleBetween(minX, maxX, true))); + } + Point2DWriter writer = new Point2DWriter(new MultiPoint(points)); + + BytesStreamOutput output = new BytesStreamOutput(); + writer.writeTo(output); + output.close(); + Point2DReader reader = new Point2DReader(new ByteBufferStreamInput(ByteBuffer.wrap(output.bytes().toBytesRef().bytes))); + assertTrue(reader.intersects(extent)); + } + } +} diff --git a/server/src/test/java/org/elasticsearch/common/geo/builders/PolygonBuilderTests.java b/server/src/test/java/org/elasticsearch/common/geo/builders/PolygonBuilderTests.java index 0d4f142785484..6095b0f772834 100644 --- a/server/src/test/java/org/elasticsearch/common/geo/builders/PolygonBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/common/geo/builders/PolygonBuilderTests.java @@ -33,6 +33,7 @@ public class PolygonBuilderTests extends AbstractShapeBuilderTestCase