diff --git a/server/src/main/java/org/elasticsearch/common/geo/TriangleTreeReader.java b/server/src/main/java/org/elasticsearch/common/geo/TriangleTreeReader.java index cecbf227a3975..b6b06e5088162 100644 --- a/server/src/main/java/org/elasticsearch/common/geo/TriangleTreeReader.java +++ b/server/src/main/java/org/elasticsearch/common/geo/TriangleTreeReader.java @@ -322,7 +322,6 @@ private GeoRelation relateTriangle(int aX, int aY, boolean ab, int bX, int bY, b int tMinY = StrictMath.min(StrictMath.min(aY, bY), cY); int tMaxY = StrictMath.max(StrictMath.max(aY, bY), cY); - // 1. check bounding boxes are disjoint, where north and east boundaries are not considered as crossing if (tMaxX <= minX || tMinX > maxX || tMinY > maxY || tMaxY <= minY) { return GeoRelation.QUERY_DISJOINT; diff --git a/server/src/main/java/org/elasticsearch/index/fielddata/MultiGeoValues.java b/server/src/main/java/org/elasticsearch/index/fielddata/MultiGeoValues.java index ea973fa91fa5f..e5bb77b4b20ca 100644 --- a/server/src/main/java/org/elasticsearch/index/fielddata/MultiGeoValues.java +++ b/server/src/main/java/org/elasticsearch/index/fielddata/MultiGeoValues.java @@ -316,6 +316,5 @@ public double minX() { public double maxX() { return Math.max(negRight, posRight); } - } } diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/GeoTileGridValuesSourceBuilder.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/GeoTileGridValuesSourceBuilder.java index e06df02518e83..b47ae3029bffd 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/GeoTileGridValuesSourceBuilder.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/GeoTileGridValuesSourceBuilder.java @@ -31,9 +31,11 @@ import org.elasticsearch.index.mapper.MappedFieldType; import org.elasticsearch.index.query.QueryShardContext; import org.elasticsearch.search.DocValueFormat; +import org.elasticsearch.search.aggregations.bucket.geogrid.BoundedGeoTileGridTiler; import org.elasticsearch.search.aggregations.bucket.geogrid.CellIdSource; import org.elasticsearch.search.aggregations.bucket.geogrid.GeoGridTiler; import org.elasticsearch.search.aggregations.bucket.geogrid.GeoTileGridAggregationBuilder; +import org.elasticsearch.search.aggregations.bucket.geogrid.GeoTileGridTiler; import org.elasticsearch.search.aggregations.bucket.geogrid.GeoTileUtils; import org.elasticsearch.search.aggregations.support.ValueType; import org.elasticsearch.search.aggregations.support.ValuesSource; @@ -138,7 +140,15 @@ protected CompositeValuesSourceConfig innerBuild(QueryShardContext queryShardCon ValuesSource.Geo geoValue = (ValuesSource.Geo) orig; // is specified in the builder. final MappedFieldType fieldType = config.fieldContext() != null ? config.fieldContext().fieldType() : null; - CellIdSource cellIdSource = new CellIdSource(geoValue, precision, geoBoundingBox, GeoGridTiler.GeoTileGridTiler.INSTANCE); + + final GeoGridTiler tiler; + if (geoBoundingBox.isUnbounded()) { + tiler = new GeoTileGridTiler(); + } else { + tiler = new BoundedGeoTileGridTiler(geoBoundingBox); + } + + CellIdSource cellIdSource = new CellIdSource(geoValue, precision, tiler); return new CompositeValuesSourceConfig(name, fieldType, cellIdSource, DocValueFormat.GEOTILE, order(), missingBucket(), script() != null); } else { diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/BoundedGeoHashGridTiler.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/BoundedGeoHashGridTiler.java new file mode 100644 index 0000000000000..0636de647a22c --- /dev/null +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/BoundedGeoHashGridTiler.java @@ -0,0 +1,108 @@ +/* + * 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.search.aggregations.bucket.geogrid; + +import org.elasticsearch.common.geo.GeoBoundingBox; +import org.elasticsearch.common.geo.GeoRelation; +import org.elasticsearch.geometry.Rectangle; +import org.elasticsearch.geometry.utils.Geohash; +import org.elasticsearch.index.fielddata.MultiGeoValues; + +public class BoundedGeoHashGridTiler extends GeoHashGridTiler { + private final double boundsTop; + private final double boundsBottom; + private final double boundsWestLeft; + private final double boundsWestRight; + private final double boundsEastLeft; + private final double boundsEastRight; + private final boolean crossesDateline; + + BoundedGeoHashGridTiler(GeoBoundingBox geoBoundingBox) { + // split geoBoundingBox into west and east boxes + boundsTop = geoBoundingBox.top(); + boundsBottom = geoBoundingBox.bottom(); + if (geoBoundingBox.right() < geoBoundingBox.left()) { + boundsWestLeft = -180; + boundsWestRight = geoBoundingBox.right(); + boundsEastLeft = geoBoundingBox.left(); + boundsEastRight = 180; + crossesDateline = true; + } else { // only set east bounds + boundsEastLeft = geoBoundingBox.left(); + boundsEastRight = geoBoundingBox.right(); + boundsWestLeft = 0; + boundsWestRight = 0; + crossesDateline = false; + } + } + + @Override + public int advancePointValue(long[] values, double x, double y, int precision, int valuesIdx) { + long hash = encode(x, y, precision); + if (cellIntersectsGeoBoundingBox(Geohash.toBoundingBox(Geohash.stringEncode(hash)))) { + values[valuesIdx] = hash; + return valuesIdx + 1; + } + return valuesIdx; + } + + boolean cellIntersectsGeoBoundingBox(Rectangle rectangle) { + return (boundsTop >= rectangle.getMinY() && boundsBottom <= rectangle.getMaxY() + && (boundsEastLeft <= rectangle.getMaxX() && boundsEastRight >= rectangle.getMinX() + || (crossesDateline && boundsWestLeft <= rectangle.getMaxX() && boundsWestRight >= rectangle.getMinX()))); + } + + @Override + protected int setValue(CellValues docValues, MultiGeoValues.GeoValue geoValue, MultiGeoValues.BoundingBox bounds, int precision) { + String hash = Geohash.stringEncode(bounds.minX(), bounds.minY(), precision); + GeoRelation relation = relateTile(geoValue, hash); + if (relation != GeoRelation.QUERY_DISJOINT) { + docValues.resizeCell(1); + docValues.add(0, Geohash.longEncode(hash)); + return 1; + } + return 0; + } + + @Override + protected GeoRelation relateTile(MultiGeoValues.GeoValue geoValue, String hash) { + Rectangle rectangle = Geohash.toBoundingBox(hash); + if (cellIntersectsGeoBoundingBox(rectangle)) { + return geoValue.relate(rectangle); + } else { + return GeoRelation.QUERY_DISJOINT; + } + } + + @Override + protected int setValuesForFullyContainedTile(String hash, CellValues values, + int valuesIndex, int targetPrecision) { + String[] hashes = Geohash.getSubGeohashes(hash); + for (int i = 0; i < hashes.length; i++) { + if (hashes[i].length() == targetPrecision ) { + if (cellIntersectsGeoBoundingBox(Geohash.toBoundingBox(hashes[i]))) { + values.add(valuesIndex++, Geohash.longEncode(hashes[i])); + } + } else { + valuesIndex = setValuesForFullyContainedTile(hashes[i], values, valuesIndex, targetPrecision); + } + } + return valuesIndex; + } +} diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/BoundedGeoPointCellValues.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/BoundedGeoPointCellValues.java deleted file mode 100644 index 493a9aa729678..0000000000000 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/BoundedGeoPointCellValues.java +++ /dev/null @@ -1,48 +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.search.aggregations.bucket.geogrid; - -import org.elasticsearch.common.geo.GeoBoundingBox; -import org.elasticsearch.index.fielddata.MultiGeoValues; - -/** - * Class representing {@link CellValues} whose values are filtered - * according to whether they are within the specified {@link GeoBoundingBox}. - * - * The specified bounding box is assumed to be bounded. - */ -class BoundedGeoPointCellValues extends CellValues { - - private final GeoBoundingBox geoBoundingBox; - - protected BoundedGeoPointCellValues(MultiGeoValues geoValues, int precision, GeoGridTiler tiler, GeoBoundingBox geoBoundingBox) { - super(geoValues, precision, tiler); - this.geoBoundingBox = geoBoundingBox; - } - - - @Override - int advanceValue(MultiGeoValues.GeoValue target, int valuesIdx) { - if (geoBoundingBox.pointInBounds(target.lon(), target.lat())) { - values[valuesIdx] = tiler.encode(target.lon(), target.lat(), precision); - return valuesIdx + 1; - } - return valuesIdx; - } -} diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/BoundedGeoTileGridTiler.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/BoundedGeoTileGridTiler.java new file mode 100644 index 0000000000000..89b6b69c41ba7 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/BoundedGeoTileGridTiler.java @@ -0,0 +1,107 @@ +/* + * 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.search.aggregations.bucket.geogrid; + +import org.elasticsearch.common.geo.GeoBoundingBox; +import org.elasticsearch.common.geo.GeoRelation; +import org.elasticsearch.geometry.Rectangle; +import org.elasticsearch.index.fielddata.MultiGeoValues; + +public class BoundedGeoTileGridTiler extends GeoTileGridTiler { + private final double boundsTop; + private final double boundsBottom; + private final double boundsWestLeft; + private final double boundsWestRight; + private final double boundsEastLeft; + private final double boundsEastRight; + private final boolean crossesDateline; + + public BoundedGeoTileGridTiler(GeoBoundingBox geoBoundingBox) { + // split geoBoundingBox into west and east boxes + boundsTop = geoBoundingBox.top(); + boundsBottom = geoBoundingBox.bottom(); + if (geoBoundingBox.right() < geoBoundingBox.left()) { + boundsWestLeft = -180; + boundsWestRight = geoBoundingBox.right(); + boundsEastLeft = geoBoundingBox.left(); + boundsEastRight = 180; + crossesDateline = true; + } else { // only set east bounds + boundsEastLeft = geoBoundingBox.left(); + boundsEastRight = geoBoundingBox.right(); + boundsWestLeft = 0; + boundsWestRight = 0; + crossesDateline = false; + } + } + + public int advancePointValue(long[] values, double x, double y, int precision, int valuesIdx) { + long hash = encode(x, y, precision); + if (cellIntersectsGeoBoundingBox(GeoTileUtils.toBoundingBox(hash))) { + values[valuesIdx] = hash; + return valuesIdx + 1; + } + return valuesIdx; + } + + boolean cellIntersectsGeoBoundingBox(Rectangle rectangle) { + return (boundsTop >= rectangle.getMinY() && boundsBottom <= rectangle.getMaxY() + && (boundsEastLeft <= rectangle.getMaxX() && boundsEastRight >= rectangle.getMinX() + || (crossesDateline && boundsWestLeft <= rectangle.getMaxX() && boundsWestRight >= rectangle.getMinX()))); + } + + @Override + public GeoRelation relateTile(MultiGeoValues.GeoValue geoValue, int xTile, int yTile, int precision) { + Rectangle rectangle = GeoTileUtils.toBoundingBox(xTile, yTile, precision); + if (cellIntersectsGeoBoundingBox(rectangle)) { + return geoValue.relate(rectangle); + } + return GeoRelation.QUERY_DISJOINT; + } + + @Override + protected int setValue(CellValues docValues, MultiGeoValues.GeoValue geoValue, int xTile, int yTile, int precision) { + if (cellIntersectsGeoBoundingBox(GeoTileUtils.toBoundingBox(xTile, yTile, precision))) { + docValues.resizeCell(1); + docValues.add(0, GeoTileUtils.longEncodeTiles(precision, xTile, yTile)); + return 1; + } + return 0; + } + + @Override + protected int setValuesForFullyContainedTile(int xTile, int yTile, int zTile, CellValues values, int valuesIndex, + int targetPrecision) { + zTile++; + for (int i = 0; i < 2; i++) { + for (int j = 0; j < 2; j++) { + int nextX = 2 * xTile + i; + int nextY = 2 * yTile + j; + if (zTile == targetPrecision) { + if (cellIntersectsGeoBoundingBox(GeoTileUtils.toBoundingBox(nextX, nextY, zTile))) { + values.add(valuesIndex++, GeoTileUtils.longEncodeTiles(zTile, nextX, nextY)); + } + } else { + valuesIndex = setValuesForFullyContainedTile(nextX, nextY, zTile, values, valuesIndex, targetPrecision); + } + } + } + return valuesIndex; + } +} diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/CellIdSource.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/CellIdSource.java index 8161aef98746c..8267bc69f8c52 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/CellIdSource.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/CellIdSource.java @@ -21,7 +21,6 @@ import org.apache.lucene.index.LeafReaderContext; import org.apache.lucene.index.SortedNumericDocValues; import org.elasticsearch.index.fielddata.MultiGeoValues; -import org.elasticsearch.common.geo.GeoBoundingBox; import org.elasticsearch.index.fielddata.SortedBinaryDocValues; import org.elasticsearch.index.fielddata.SortedNumericDoubleValues; import org.elasticsearch.search.aggregations.support.CoreValuesSourceType; @@ -36,13 +35,11 @@ public class CellIdSource extends ValuesSource.Numeric { private final ValuesSource.Geo valuesSource; private final int precision; private final GeoGridTiler encoder; - private final GeoBoundingBox geoBoundingBox; - public CellIdSource(ValuesSource.Geo valuesSource, int precision, GeoBoundingBox geoBoundingBox, GeoGridTiler encoder) { + public CellIdSource(ValuesSource.Geo valuesSource, int precision, GeoGridTiler encoder) { this.valuesSource = valuesSource; //different GeoPoints could map to the same or different hashing cells. this.precision = precision; - this.geoBoundingBox = geoBoundingBox; this.encoder = encoder; } @@ -65,19 +62,10 @@ public SortedNumericDocValues longValues(LeafReaderContext ctx) { ValuesSourceType vs = geoValues.valuesSourceType(); if (CoreValuesSourceType.GEOPOINT == vs) { // docValues are geo points - if (geoBoundingBox.isUnbounded()) { - return new UnboundedGeoPointCellValues(geoValues, precision, encoder); - } else { - return new BoundedGeoPointCellValues(geoValues, precision, encoder, geoBoundingBox); - } + return new GeoPointCellValues(geoValues, precision, encoder); } else if (CoreValuesSourceType.GEOSHAPE == vs || CoreValuesSourceType.GEO == vs) { // docValues are geo shapes - if (geoBoundingBox.isUnbounded()) { - return new GeoShapeCellValues(geoValues, precision, encoder); - } else { - // TODO(talevy): support unbounded - throw new IllegalArgumentException("bounded geogrid is not supported on geo_shape fields"); - } + return new GeoShapeCellValues(geoValues, precision, encoder); } else { throw new IllegalArgumentException("unsupported geo type"); } diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/CellValues.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/CellValues.java index b53d02e81a2ef..6ebad166f513d 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/CellValues.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/CellValues.java @@ -56,6 +56,19 @@ public boolean advanceExact(int docId) throws IOException { } } + // for testing + protected long[] getValues() { + return values; + } + + protected void add(int idx, long value) { + values[idx] = value; + } + + void resizeCell(int newSize) { + resize(newSize); + } + /** * Sets the appropriate long-encoded value for target * in values. diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoGridTiler.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoGridTiler.java index ebf7ad21d208c..1f98f9bdf3031 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoGridTiler.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoGridTiler.java @@ -19,9 +19,6 @@ package org.elasticsearch.search.aggregations.bucket.geogrid; -import org.elasticsearch.common.geo.GeoRelation; -import org.elasticsearch.geometry.Rectangle; -import org.elasticsearch.geometry.utils.Geohash; import org.elasticsearch.index.fielddata.MultiGeoValues; /** @@ -39,241 +36,31 @@ public interface GeoGridTiler { /** * - * @param docValues the array of long-encoded bucket keys to fill - * @param geoValue the input shape - * @param precision the tile zoom-level + * @param docValues the array of long-encoded bucket keys to fill + * @param geoValue the input shape + * @param precision the tile zoom-level * * @return the number of tiles the geoValue intersects */ - int setValues(GeoShapeCellValues docValues, MultiGeoValues.GeoValue geoValue, int precision); + int setValues(CellValues docValues, MultiGeoValues.GeoValue geoValue, int precision); - class GeoHashGridTiler implements GeoGridTiler { - public static final GeoHashGridTiler INSTANCE = new GeoHashGridTiler(); - private GeoHashGridTiler() {} - - @Override - public long encode(double x, double y, int precision) { - return Geohash.longEncode(x, y, precision); - } - - @Override - public int setValues(GeoShapeCellValues values, MultiGeoValues.GeoValue geoValue, int precision) { - if (precision == 1) { - values.resizeCell(1); - values.add(0, Geohash.longEncode(0, 0, 0)); - } - - MultiGeoValues.BoundingBox bounds = geoValue.boundingBox(); - assert bounds.minX() <= bounds.maxX(); - long numLonCells = (long) ((bounds.maxX() - bounds.minX()) / Geohash.lonWidthInDegrees(precision)); - long numLatCells = (long) ((bounds.maxY() - bounds.minY()) / Geohash.latHeightInDegrees(precision)); - long count = (numLonCells + 1) * (numLatCells + 1); - if (count == 1) { - String hash = Geohash.stringEncode(bounds.minX(), bounds.minY(), precision); - values.resizeCell(1); - values.add(0, Geohash.longEncode(hash)); - return 1; - } else if (count <= precision) { - return setValuesByBruteForceScan(values, geoValue, precision, bounds); - } else { - return setValuesByRasterization("", values, 0, precision, geoValue); - } - } - - protected int setValuesByBruteForceScan(GeoShapeCellValues values, MultiGeoValues.GeoValue geoValue, - int precision, MultiGeoValues.BoundingBox bounds) { - // TODO: This way to discover cells inside of a bounding box seems not to work as expected. I can - // see that eventually we will be visiting twice the same cell which should not happen. - int idx = 0; - String min = Geohash.stringEncode(bounds.minX(), bounds.minY(), precision); - String max = Geohash.stringEncode(bounds.maxX(), bounds.maxY(), precision); - String minNeighborBelow = Geohash.getNeighbor(min, precision, 0, -1); - double minY = Geohash.decodeLatitude((minNeighborBelow == null) ? min : minNeighborBelow); - double minX = Geohash.decodeLongitude(min); - double maxY = Geohash.decodeLatitude(max); - double maxX = Geohash.decodeLongitude(max); - for (double i = minX; i <= maxX; i += Geohash.lonWidthInDegrees(precision)) { - for (double j = minY; j <= maxY; j += Geohash.latHeightInDegrees(precision)) { - Rectangle rectangle = Geohash.toBoundingBox(Geohash.stringEncode(i, j, precision)); - GeoRelation relation = geoValue.relate(rectangle); - if (relation != GeoRelation.QUERY_DISJOINT) { - values.resizeCell(idx + 1); - values.add(idx++, encode(i, j, precision)); - } - } - } - return idx; - } - - protected int setValuesByRasterization(String hash, GeoShapeCellValues values, int valuesIndex, - int targetPrecision, MultiGeoValues.GeoValue geoValue) { - String[] hashes = Geohash.getSubGeohashes(hash); - for (int i = 0; i < hashes.length; i++) { - Rectangle rectangle = Geohash.toBoundingBox(hashes[i]); - GeoRelation relation = geoValue.relate(rectangle); - if (relation == GeoRelation.QUERY_CROSSES) { - if (hashes[i].length() == targetPrecision) { - values.resizeCell(valuesIndex + 1); - values.add(valuesIndex++, Geohash.longEncode(hashes[i])); - } else { - valuesIndex = - setValuesByRasterization(hashes[i], values, valuesIndex, targetPrecision, geoValue); - } - } else if (relation == GeoRelation.QUERY_INSIDE) { - if (hashes[i].length() == targetPrecision) { - values.resizeCell(valuesIndex + 1); - values.add(valuesIndex++, Geohash.longEncode(hashes[i])); - } else { - values.resizeCell(valuesIndex + (int) Math.pow(32, targetPrecision - hash.length()) + 1); - valuesIndex = setValuesForFullyContainedTile(hashes[i],values, valuesIndex, targetPrecision); - } - } - } - return valuesIndex; - } - - private int setValuesForFullyContainedTile(String hash, GeoShapeCellValues values, - int valuesIndex, int targetPrecision) { - String[] hashes = Geohash.getSubGeohashes(hash); - for (int i = 0; i < hashes.length; i++) { - if (hashes[i].length() == targetPrecision) { - values.add(valuesIndex++, Geohash.longEncode(hashes[i])); - } else { - valuesIndex = setValuesForFullyContainedTile(hashes[i], values, valuesIndex, targetPrecision); - } - } - return valuesIndex; - } - } - - class GeoTileGridTiler implements GeoGridTiler { - public static final GeoTileGridTiler INSTANCE = new GeoTileGridTiler(); - - private GeoTileGridTiler() {} - - @Override - public long encode(double x, double y, int precision) { - return GeoTileUtils.longEncode(x, y, precision); - } - - /** - * Sets the values of the long[] underlying {@link GeoShapeCellValues}. - * - * If the shape resides between GeoTileUtils.NORMALIZED_LATITUDE_MASK and 90 or - * between GeoTileUtils.NORMALIZED_NEGATIVE_LATITUDE_MASK and -90 degree latitudes, then - * the shape is not accounted for since geo-tiles are only defined within those bounds. - * - * @param values the bucket values - * @param geoValue the input shape - * @param precision the tile zoom-level - * - * @return the number of tiles set by the shape - */ - @Override - public int setValues(GeoShapeCellValues values, MultiGeoValues.GeoValue geoValue, int precision) { - MultiGeoValues.BoundingBox bounds = geoValue.boundingBox(); - assert bounds.minX() <= bounds.maxX(); - - if (precision == 0) { - values.resizeCell(1); - values.add(0, GeoTileUtils.longEncodeTiles(0, 0, 0)); - return 1; - } - - // geo tiles are not defined at the extreme latitudes due to them - // tiling the world as a square. - if ((bounds.top > GeoTileUtils.NORMALIZED_LATITUDE_MASK && bounds.bottom > GeoTileUtils.NORMALIZED_LATITUDE_MASK) - || (bounds.top < GeoTileUtils.NORMALIZED_NEGATIVE_LATITUDE_MASK - && bounds.bottom < GeoTileUtils.NORMALIZED_NEGATIVE_LATITUDE_MASK)) { - return 0; - } - - - final double tiles = 1 << precision; - int minXTile = GeoTileUtils.getXTile(bounds.minX(), (long) tiles); - int minYTile = GeoTileUtils.getYTile(bounds.maxY(), (long) tiles); - int maxXTile = GeoTileUtils.getXTile(bounds.maxX(), (long) tiles); - int maxYTile = GeoTileUtils.getYTile(bounds.minY(), (long) tiles); - int count = (maxXTile - minXTile + 1) * (maxYTile - minYTile + 1); - if (count == 1) { - values.resizeCell(1); - values.add(0, GeoTileUtils.longEncodeTiles(precision, minXTile, minYTile)); - return 1; - } else if (count <= precision) { - return setValuesByBruteForceScan(values, geoValue, precision, minXTile, minYTile, maxXTile, maxYTile); - } else { - return setValuesByRasterization(0, 0, 0, values, 0, precision, geoValue); - } - } - - /** - * - * @param values the bucket values as longs - * @param geoValue the shape value - * @param precision the target precision to split the shape up into - * @return the number of buckets the geoValue is found in - */ - protected int setValuesByBruteForceScan(GeoShapeCellValues values, MultiGeoValues.GeoValue geoValue, - int precision, int minXTile, int minYTile, int maxXTile, int maxYTile) { - int idx = 0; - for (int i = minXTile; i <= maxXTile; i++) { - for (int j = minYTile; j <= maxYTile; j++) { - Rectangle rectangle = GeoTileUtils.toBoundingBox(i, j, precision); - if (geoValue.relate(rectangle) != GeoRelation.QUERY_DISJOINT) { - values.resizeCell(idx + 1); - values.add(idx++, GeoTileUtils.longEncodeTiles(precision, i, j)); - } - } - } - return idx; - } - - protected int setValuesByRasterization(int xTile, int yTile, int zTile, GeoShapeCellValues values, - int valuesIndex, int targetPrecision, MultiGeoValues.GeoValue geoValue) { - zTile++; - for (int i = 0; i < 2; i++) { - for (int j = 0; j < 2; j++) { - int nextX = 2 * xTile + i; - int nextY = 2 * yTile + j; - Rectangle rectangle = GeoTileUtils.toBoundingBox(nextX, nextY, zTile); - GeoRelation relation = geoValue.relate(rectangle); - if (GeoRelation.QUERY_INSIDE == relation) { - if (zTile == targetPrecision) { - values.resizeCell(valuesIndex + 1); - values.add(valuesIndex++, GeoTileUtils.longEncodeTiles(zTile, nextX, nextY)); - } else { - values.resizeCell(valuesIndex + 1 << ( 2 * (targetPrecision - zTile)) + 1); - valuesIndex = setValuesForFullyContainedTile(nextX, nextY, zTile, values, valuesIndex, targetPrecision); - } - } else if (GeoRelation.QUERY_CROSSES == relation) { - if (zTile == targetPrecision) { - values.resizeCell(valuesIndex + 1); - values.add(valuesIndex++, GeoTileUtils.longEncodeTiles(zTile, nextX, nextY)); - } else { - valuesIndex = setValuesByRasterization(nextX, nextY, zTile, values, valuesIndex, targetPrecision, geoValue); - } - } - } - } - return valuesIndex; - } - - private int setValuesForFullyContainedTile(int xTile, int yTile, int zTile, - GeoShapeCellValues values, int valuesIndex, int targetPrecision) { - zTile++; - for (int i = 0; i < 2; i++) { - for (int j = 0; j < 2; j++) { - int nextX = 2 * xTile + i; - int nextY = 2 * yTile + j; - if (zTile == targetPrecision) { - values.add(valuesIndex++, GeoTileUtils.longEncodeTiles(zTile, nextX, nextY)); - } else { - valuesIndex = setValuesForFullyContainedTile(nextX, nextY, zTile, values, valuesIndex, targetPrecision); - } - } - } - return valuesIndex; - } + /** + * This sets the long-encoded value of the geo-point into the associated doc-values + * array. This is to be overridden by the {@link BoundedGeoTileGridTiler} and + * {@link BoundedGeoHashGridTiler} to check whether the point's tile intersects + * the appropriate bounds. + * + * @param values the doc-values array + * @param x the longitude of the point + * @param y the latitude of the point + * @param precision the zoom-level + * @param valuesIdx the index into the doc-values array at the time of advancement + * + * @return the next index into the array + */ + default int advancePointValue(long[] values, double x, double y, int precision, int valuesIdx) { + values[valuesIdx] = encode(x, y, precision); + return valuesIdx + 1; } } diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoHashGridAggregatorFactory.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoHashGridAggregatorFactory.java index 153602493c3ba..0999732dc31d9 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoHashGridAggregatorFactory.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoHashGridAggregatorFactory.java @@ -80,7 +80,13 @@ protected Aggregator doCreateInternal(final ValuesSource.Geo valuesSource, if (collectsFromSingleBucket == false) { return asMultiBucketAggregator(this, searchContext, parent); } - CellIdSource cellIdSource = new CellIdSource(valuesSource, precision, geoBoundingBox, GeoGridTiler.GeoHashGridTiler.INSTANCE); + final GeoGridTiler tiler; + if (geoBoundingBox.isUnbounded()) { + tiler = new GeoHashGridTiler(); + } else { + tiler = new BoundedGeoHashGridTiler(geoBoundingBox); + } + CellIdSource cellIdSource = new CellIdSource(valuesSource, precision, tiler); return new GeoHashGridAggregator(name, factories, cellIdSource, requiredSize, shardSize, searchContext, parent, pipelineAggregators, metaData); } diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoHashGridTiler.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoHashGridTiler.java new file mode 100644 index 0000000000000..7e60e7752d58c --- /dev/null +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoHashGridTiler.java @@ -0,0 +1,133 @@ +/* + * 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.search.aggregations.bucket.geogrid; + +import org.elasticsearch.common.geo.GeoRelation; +import org.elasticsearch.geometry.Rectangle; +import org.elasticsearch.geometry.utils.Geohash; +import org.elasticsearch.index.fielddata.MultiGeoValues; + +public class GeoHashGridTiler implements GeoGridTiler { + + @Override + public long encode(double x, double y, int precision) { + return Geohash.longEncode(x, y, precision); + } + + @Override + public int setValues(CellValues values, MultiGeoValues.GeoValue geoValue, int precision) { + if (precision == 1) { + values.resizeCell(1); + values.add(0, Geohash.longEncode(0, 0, 0)); + } + + MultiGeoValues.BoundingBox bounds = geoValue.boundingBox(); + assert bounds.minX() <= bounds.maxX(); + long numLonCells = (long) ((bounds.maxX() - bounds.minX()) / Geohash.lonWidthInDegrees(precision)); + long numLatCells = (long) ((bounds.maxY() - bounds.minY()) / Geohash.latHeightInDegrees(precision)); + long count = (numLonCells + 1) * (numLatCells + 1); + if (count == 1) { + return setValue(values, geoValue, bounds, precision); + } else if (count <= precision) { + return setValuesByBruteForceScan(values, geoValue, precision, bounds); + } else { + return setValuesByRasterization("", values, 0, precision, geoValue); + } + } + + /** + * Sets a singular doc-value for the {@link MultiGeoValues.GeoValue}. To be overriden by {@link BoundedGeoHashGridTiler} + * to account for {@link org.elasticsearch.common.geo.GeoBoundingBox} conditions + */ + protected int setValue(CellValues docValues, MultiGeoValues.GeoValue geoValue, MultiGeoValues.BoundingBox bounds, int precision) { + String hash = Geohash.stringEncode(bounds.minX(), bounds.minY(), precision); + docValues.resizeCell(1); + docValues.add(0, Geohash.longEncode(hash)); + return 1; + } + + protected GeoRelation relateTile(MultiGeoValues.GeoValue geoValue, String hash) { + Rectangle rectangle = Geohash.toBoundingBox(hash); + return geoValue.relate(rectangle); + } + + protected int setValuesByBruteForceScan(CellValues values, MultiGeoValues.GeoValue geoValue, int precision, + MultiGeoValues.BoundingBox bounds) { + // TODO: This way to discover cells inside of a bounding box seems not to work as expected. I can + // see that eventually we will be visiting twice the same cell which should not happen. + int idx = 0; + String min = Geohash.stringEncode(bounds.minX(), bounds.minY(), precision); + String max = Geohash.stringEncode(bounds.maxX(), bounds.maxY(), precision); + String minNeighborBelow = Geohash.getNeighbor(min, precision, 0, -1); + double minY = Geohash.decodeLatitude((minNeighborBelow == null) ? min : minNeighborBelow); + double minX = Geohash.decodeLongitude(min); + double maxY = Geohash.decodeLatitude(max); + double maxX = Geohash.decodeLongitude(max); + for (double i = minX; i <= maxX; i += Geohash.lonWidthInDegrees(precision)) { + for (double j = minY; j <= maxY; j += Geohash.latHeightInDegrees(precision)) { + String hash = Geohash.stringEncode(i, j, precision); + GeoRelation relation = relateTile(geoValue, hash); + if (relation != GeoRelation.QUERY_DISJOINT) { + values.resizeCell(idx + 1); + values.add(idx++, encode(i, j, precision)); + } + } + } + return idx; + } + + protected int setValuesByRasterization(String hash, CellValues values, int valuesIndex, int targetPrecision, + MultiGeoValues.GeoValue geoValue) { + String[] hashes = Geohash.getSubGeohashes(hash); + for (int i = 0; i < hashes.length; i++) { + GeoRelation relation = relateTile(geoValue, hashes[i]); + if (relation == GeoRelation.QUERY_CROSSES) { + if (hashes[i].length() == targetPrecision) { + values.resizeCell(valuesIndex + 1); + values.add(valuesIndex++, Geohash.longEncode(hashes[i])); + } else { + valuesIndex = + setValuesByRasterization(hashes[i], values, valuesIndex, targetPrecision, geoValue); + } + } else if (relation == GeoRelation.QUERY_INSIDE) { + if (hashes[i].length() == targetPrecision) { + values.resizeCell(valuesIndex + 1); + values.add(valuesIndex++, Geohash.longEncode(hashes[i])); + } else { + values.resizeCell(valuesIndex + (int) Math.pow(32, targetPrecision - hash.length()) + 1); + valuesIndex = setValuesForFullyContainedTile(hashes[i],values, valuesIndex, targetPrecision); + } + } + } + return valuesIndex; + } + + protected int setValuesForFullyContainedTile(String hash, CellValues values, + int valuesIndex, int targetPrecision) { + String[] hashes = Geohash.getSubGeohashes(hash); + for (int i = 0; i < hashes.length; i++) { + if (hashes[i].length() == targetPrecision) { + values.add(valuesIndex++, Geohash.longEncode(hashes[i])); + } else { + valuesIndex = setValuesForFullyContainedTile(hashes[i], values, valuesIndex, targetPrecision); + } + } + return valuesIndex; + } +} diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/UnboundedGeoPointCellValues.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoPointCellValues.java similarity index 71% rename from server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/UnboundedGeoPointCellValues.java rename to server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoPointCellValues.java index 3ce9cac197158..57e7311eee38c 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/UnboundedGeoPointCellValues.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoPointCellValues.java @@ -18,22 +18,19 @@ */ package org.elasticsearch.search.aggregations.bucket.geogrid; -import org.elasticsearch.common.geo.GeoBoundingBox; import org.elasticsearch.index.fielddata.MultiGeoValues; /** - * Class representing {@link CellValues} that are unbounded by any - * {@link GeoBoundingBox}. + * Class representing geo_point {@link CellValues} */ -class UnboundedGeoPointCellValues extends CellValues { +class GeoPointCellValues extends CellValues { - protected UnboundedGeoPointCellValues(MultiGeoValues geoValues, int precision, GeoGridTiler tiler) { + protected GeoPointCellValues(MultiGeoValues geoValues, int precision, GeoGridTiler tiler) { super(geoValues, precision, tiler); } @Override int advanceValue(MultiGeoValues.GeoValue target, int valuesIdx) { - values[valuesIdx] = tiler.encode(target.lon(), target.lat(), precision); - return valuesIdx + 1; + return tiler.advancePointValue(values, target.lon(), target.lat(), precision, valuesIdx); } } diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoShapeCellValues.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoShapeCellValues.java index 807b2808b9a45..22ed207a41338 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoShapeCellValues.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoShapeCellValues.java @@ -18,49 +18,18 @@ */ package org.elasticsearch.search.aggregations.bucket.geogrid; -import org.elasticsearch.index.fielddata.AbstractSortingNumericDocValues; import org.elasticsearch.index.fielddata.MultiGeoValues; -import org.elasticsearch.search.aggregations.support.ValuesSourceType; - -import java.io.IOException; /** Sorted numeric doc values for geo shapes */ -class GeoShapeCellValues extends AbstractSortingNumericDocValues { - private MultiGeoValues geoValues; - private int precision; - private GeoGridTiler tiler; +class GeoShapeCellValues extends CellValues { protected GeoShapeCellValues(MultiGeoValues geoValues, int precision, GeoGridTiler tiler) { - this.geoValues = geoValues; - this.precision = precision; - this.tiler = tiler; - } - - protected void resizeCell(int newSize) { - resize(newSize); - } - - protected void add(int idx, long value) { - values[idx] = value; - } - - // for testing - protected long[] getValues() { - return values; + super(geoValues, precision, tiler); } @Override - public boolean advanceExact(int docId) throws IOException { - if (geoValues.advanceExact(docId)) { - ValuesSourceType vs = geoValues.valuesSourceType(); - MultiGeoValues.GeoValue target = geoValues.nextValue(); - // TODO(talevy): determine reasonable circuit-breaker here - resize(0); - tiler.setValues(this, target, precision); - sort(); - return true; - } else { - return false; - } + int advanceValue(MultiGeoValues.GeoValue target, int valuesIdx) { + // TODO(talevy): determine reasonable circuit-breaker here + return tiler.setValues(this, target, precision); } } diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoTileGridAggregatorFactory.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoTileGridAggregatorFactory.java index 54b29b01a8f20..680639e49a70d 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoTileGridAggregatorFactory.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoTileGridAggregatorFactory.java @@ -80,7 +80,13 @@ protected Aggregator doCreateInternal(final ValuesSource.Geo valuesSource, if (collectsFromSingleBucket == false) { return asMultiBucketAggregator(this, searchContext, parent); } - CellIdSource cellIdSource = new CellIdSource(valuesSource, precision, geoBoundingBox, GeoGridTiler.GeoTileGridTiler.INSTANCE); + final GeoGridTiler tiler; + if (geoBoundingBox.isUnbounded()) { + tiler = new GeoTileGridTiler(); + } else { + tiler = new BoundedGeoTileGridTiler(geoBoundingBox); + } + CellIdSource cellIdSource = new CellIdSource(valuesSource, precision, tiler); return new GeoTileGridAggregator(name, factories, cellIdSource, requiredSize, shardSize, searchContext, parent, pipelineAggregators, metaData); } diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoTileGridTiler.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoTileGridTiler.java new file mode 100644 index 0000000000000..869c14c24e252 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoTileGridTiler.java @@ -0,0 +1,166 @@ +/* + * 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.search.aggregations.bucket.geogrid; + +import org.elasticsearch.common.geo.GeoRelation; +import org.elasticsearch.geometry.Rectangle; +import org.elasticsearch.index.fielddata.MultiGeoValues; + +public class GeoTileGridTiler implements GeoGridTiler { + + @Override + public long encode(double x, double y, int precision) { + return GeoTileUtils.longEncode(x, y, precision); + } + + public int advancePointValue(long[] values, double x, double y, int precision, int valuesIdx) { + values[valuesIdx] = encode(x, y, precision); + return valuesIdx + 1; + } + + /** + * Sets the values of the long[] underlying {@link CellValues}. + * + * If the shape resides between GeoTileUtils.NORMALIZED_LATITUDE_MASK and 90 or + * between GeoTileUtils.NORMALIZED_NEGATIVE_LATITUDE_MASK and -90 degree latitudes, then + * the shape is not accounted for since geo-tiles are only defined within those bounds. + * + * @param values the bucket values + * @param geoValue the input shape + * @param precision the tile zoom-level + * + * @return the number of tiles set by the shape + */ + @Override + public int setValues(CellValues values, MultiGeoValues.GeoValue geoValue, int precision) { + MultiGeoValues.BoundingBox bounds = geoValue.boundingBox(); + assert bounds.minX() <= bounds.maxX(); + + if (precision == 0) { + values.resizeCell(1); + values.add(0, GeoTileUtils.longEncodeTiles(0, 0, 0)); + return 1; + } + + // geo tiles are not defined at the extreme latitudes due to them + // tiling the world as a square. + if ((bounds.top > GeoTileUtils.NORMALIZED_LATITUDE_MASK && bounds.bottom > GeoTileUtils.NORMALIZED_LATITUDE_MASK) + || (bounds.top < GeoTileUtils.NORMALIZED_NEGATIVE_LATITUDE_MASK + && bounds.bottom < GeoTileUtils.NORMALIZED_NEGATIVE_LATITUDE_MASK)) { + return 0; + } + + final double tiles = 1 << precision; + int minXTile = GeoTileUtils.getXTile(bounds.minX(), (long) tiles); + int minYTile = GeoTileUtils.getYTile(bounds.maxY(), (long) tiles); + int maxXTile = GeoTileUtils.getXTile(bounds.maxX(), (long) tiles); + int maxYTile = GeoTileUtils.getYTile(bounds.minY(), (long) tiles); + int count = (maxXTile - minXTile + 1) * (maxYTile - minYTile + 1); + if (count == 1) { + return setValue(values, geoValue, minXTile, minYTile, precision); + } else if (count <= precision) { + return setValuesByBruteForceScan(values, geoValue, precision, minXTile, minYTile, maxXTile, maxYTile); + } else { + return setValuesByRasterization(0, 0, 0, values, 0, precision, geoValue); + } + } + + protected GeoRelation relateTile(MultiGeoValues.GeoValue geoValue, int xTile, int yTile, int precision) { + Rectangle rectangle = GeoTileUtils.toBoundingBox(xTile, yTile, precision); + return geoValue.relate(rectangle); + } + + /** + * Sets a singular doc-value for the {@link MultiGeoValues.GeoValue}. To be overriden by {@link BoundedGeoTileGridTiler} + * to account for {@link org.elasticsearch.common.geo.GeoBoundingBox} conditions + */ + protected int setValue(CellValues docValues, MultiGeoValues.GeoValue geoValue, int xTile, int yTile, int precision) { + docValues.resizeCell(1); + docValues.add(0, GeoTileUtils.longEncodeTiles(precision, xTile, yTile)); + return 1; + } + + /** + * + * @param values the bucket values as longs + * @param geoValue the shape value + * @param precision the target precision to split the shape up into + * @return the number of buckets the geoValue is found in + */ + protected int setValuesByBruteForceScan(CellValues values, MultiGeoValues.GeoValue geoValue, + int precision, int minXTile, int minYTile, int maxXTile, int maxYTile) { + int idx = 0; + for (int i = minXTile; i <= maxXTile; i++) { + for (int j = minYTile; j <= maxYTile; j++) { + GeoRelation relation = relateTile(geoValue, i, j, precision); + if (relation != GeoRelation.QUERY_DISJOINT) { + values.resizeCell(idx + 1); + values.add(idx++, GeoTileUtils.longEncodeTiles(precision, i, j)); + } + } + } + return idx; + } + + protected int setValuesByRasterization(int xTile, int yTile, int zTile, CellValues values, int valuesIndex, + int targetPrecision, MultiGeoValues.GeoValue geoValue) { + zTile++; + for (int i = 0; i < 2; i++) { + for (int j = 0; j < 2; j++) { + int nextX = 2 * xTile + i; + int nextY = 2 * yTile + j; + GeoRelation relation = relateTile(geoValue, nextX, nextY, zTile); + if (GeoRelation.QUERY_INSIDE == relation) { + if (zTile == targetPrecision) { + values.resizeCell(valuesIndex + 1); + values.add(valuesIndex++, GeoTileUtils.longEncodeTiles(zTile, nextX, nextY)); + } else { + values.resizeCell(valuesIndex + 1 << ( 2 * (targetPrecision - zTile)) + 1); + valuesIndex = setValuesForFullyContainedTile(nextX, nextY, zTile, values, valuesIndex, targetPrecision); + } + } else if (GeoRelation.QUERY_CROSSES == relation) { + if (zTile == targetPrecision) { + values.resizeCell(valuesIndex + 1); + values.add(valuesIndex++, GeoTileUtils.longEncodeTiles(zTile, nextX, nextY)); + } else { + valuesIndex = setValuesByRasterization(nextX, nextY, zTile, values, valuesIndex, targetPrecision, geoValue); + } + } + } + } + return valuesIndex; + } + + protected int setValuesForFullyContainedTile(int xTile, int yTile, int zTile, CellValues values, int valuesIndex, + int targetPrecision) { + zTile++; + for (int i = 0; i < 2; i++) { + for (int j = 0; j < 2; j++) { + int nextX = 2 * xTile + i; + int nextY = 2 * yTile + j; + if (zTile == targetPrecision) { + values.add(valuesIndex++, GeoTileUtils.longEncodeTiles(zTile, nextX, nextY)); + } else { + valuesIndex = setValuesForFullyContainedTile(nextX, nextY, zTile, values, valuesIndex, targetPrecision); + } + } + } + return valuesIndex; + } +} diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoTileUtils.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoTileUtils.java index c97bb88135109..d872a8c7ce640 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoTileUtils.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoTileUtils.java @@ -44,23 +44,10 @@ */ public final class GeoTileUtils { - /** - * The geo-tile map is clipped at 85.05112878 to 90 and -85.05112878 to -90 - */ - public static final double LATITUDE_MASK = 85.0511287798066; - - /** - * Since shapes are encoded, their boundaries are to be compared to against the encoded/decoded values of LATITUDE_MASK - */ - static final double NORMALIZED_LATITUDE_MASK = GeoEncodingUtils.decodeLatitude(GeoEncodingUtils.encodeLatitude(LATITUDE_MASK)); - static final double NORMALIZED_NEGATIVE_LATITUDE_MASK = - GeoEncodingUtils.decodeLatitude(GeoEncodingUtils.encodeLatitude(-LATITUDE_MASK)); + private GeoTileUtils() {} private static final double PI_DIV_2 = Math.PI / 2; - - private GeoTileUtils() {} - /** * Largest number of tiles (precision) to use. * This value cannot be more than (64-5)/2 = 29, because 5 bits are used for zoom level itself (0-31) @@ -71,6 +58,18 @@ private GeoTileUtils() {} */ public static final int MAX_ZOOM = 29; + /** + * The geo-tile map is clipped at 85.05112878 to 90 and -85.05112878 to -90 + */ + public static final double LATITUDE_MASK = 85.0511287798066; + + /** + * Since shapes are encoded, their boundaries are to be compared to against the encoded/decoded values of LATITUDE_MASK + */ + static final double NORMALIZED_LATITUDE_MASK = GeoEncodingUtils.decodeLatitude(GeoEncodingUtils.encodeLatitude(LATITUDE_MASK)); + static final double NORMALIZED_NEGATIVE_LATITUDE_MASK = + GeoEncodingUtils.decodeLatitude(GeoEncodingUtils.encodeLatitude(-LATITUDE_MASK)); + /** * Bit position of the zoom value within hash - zoom is stored in the most significant 6 bits of a long number. */ @@ -81,6 +80,7 @@ private GeoTileUtils() {} */ private static final long X_Y_VALUE_MASK = (1L << MAX_ZOOM) - 1; + /** * Parse an integer precision (zoom level). The {@link ValueType#INT} allows it to be a number or a string. * @@ -246,6 +246,11 @@ static GeoPoint keyToGeoPoint(String hashAsString) { return zxyToGeoPoint(hashAsInts[0], hashAsInts[1], hashAsInts[2]); } + static Rectangle toBoundingBox(long hash) { + int[] hashAsInts = parseHash(hash); + return toBoundingBox(hashAsInts[1], hashAsInts[2], hashAsInts[0]); + } + static Rectangle toBoundingBox(int xTile, int yTile, int precision) { final double tiles = validateZXY(precision, xTile, yTile); final double minN = Math.PI - (2.0 * Math.PI * (yTile + 1)) / tiles; diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalGeoBounds.java b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalGeoBounds.java index 15585c2f4a3a8..22249d8aa6a94 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalGeoBounds.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalGeoBounds.java @@ -175,7 +175,8 @@ public XContentBuilder doXContentBody(XContentBuilder builder, Params params) th return builder; } - private GeoBoundingBox resolveGeoBoundingBox() { + // used for testing + GeoBoundingBox resolveGeoBoundingBox() { if (Double.isInfinite(top)) { return null; } else if (Double.isInfinite(posLeft)) { diff --git a/server/src/test/java/org/elasticsearch/common/geo/TriangleTreeTests.java b/server/src/test/java/org/elasticsearch/common/geo/TriangleTreeTests.java index 3de9d70d459ab..f7561508f831d 100644 --- a/server/src/test/java/org/elasticsearch/common/geo/TriangleTreeTests.java +++ b/server/src/test/java/org/elasticsearch/common/geo/TriangleTreeTests.java @@ -257,9 +257,7 @@ public void testPacManPoints() throws Exception { assertRelation(GeoRelation.QUERY_CROSSES, reader, getExtentFromBox(xMin, yMin, xMax, yMax)); } - @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/37206") public void testRandomMultiLineIntersections() throws IOException { - double extentSize = randomDoubleBetween(0.01, 10, true); GeoShapeIndexer indexer = new GeoShapeIndexer(true, "test"); MultiLine geometry = randomMultiLine(false); geometry = (MultiLine) indexer.prepareForIndexing(geometry); @@ -267,14 +265,7 @@ public void testRandomMultiLineIntersections() throws IOException { Extent readerExtent = reader.getExtent(); for (Line line : geometry) { - // extent that intersects edges - assertRelation(GeoRelation.QUERY_CROSSES, reader, bufferedExtentFromGeoPoint(line.getX(0), line.getY(0), extentSize)); - - // TODO(talevy): resolve definition. when line is on a specific edge it is not considered crossing due to latest changes - // extent that fully encloses a line in the MultiLine Extent lineExtent = triangleTreeReader(line, GeoShapeCoordinateEncoder.INSTANCE).getExtent(); - assertRelation(GeoRelation.QUERY_CROSSES, reader, lineExtent); - if (lineExtent.minX() != Integer.MIN_VALUE && lineExtent.maxX() != Integer.MAX_VALUE && lineExtent.minY() != Integer.MIN_VALUE && lineExtent.maxY() != Integer.MAX_VALUE) { assertRelation(GeoRelation.QUERY_CROSSES, reader, Extent.fromPoints(lineExtent.minX() - 1, lineExtent.minY() - 1, @@ -292,9 +283,8 @@ public void testRandomMultiLineIntersections() throws IOException { } - @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/37206") - public void testRandomGeometryIntersection() throws IOException { - int testPointCount = randomIntBetween(100, 200); + public void testRandomPolygonIntersection() throws IOException { + int testPointCount = randomIntBetween(50, 100); Point[] testPoints = new Point[testPointCount]; double extentSize = randomDoubleBetween(1, 10, true); boolean[] intersects = new boolean[testPointCount]; @@ -302,7 +292,7 @@ public void testRandomGeometryIntersection() throws IOException { testPoints[i] = randomPoint(false); } - Geometry geometry = randomGeometryTreeGeometry(); + Geometry geometry = randomMultiPolygon(false); GeoShapeIndexer indexer = new GeoShapeIndexer(true, "test"); Geometry preparedGeometry = indexer.prepareForIndexing(geometry); diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoGridAggregatorTestCase.java b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoGridAggregatorTestCase.java index 3840b3423a94e..42417db2484db 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoGridAggregatorTestCase.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoGridAggregatorTestCase.java @@ -30,14 +30,17 @@ import org.apache.lucene.store.Directory; import org.elasticsearch.common.CheckedConsumer; import org.elasticsearch.common.geo.CentroidCalculator; +import org.elasticsearch.common.geo.GeoRelation; +import org.elasticsearch.common.geo.GeoShapeCoordinateEncoder; import org.elasticsearch.common.geo.GeoTestUtils; +import org.elasticsearch.common.geo.TriangleTreeReader; import org.elasticsearch.geometry.Geometry; import org.elasticsearch.geometry.MultiPoint; import org.elasticsearch.geometry.Point; -import org.elasticsearch.index.mapper.BinaryGeoShapeDocValuesField; import org.elasticsearch.common.geo.GeoBoundingBox; import org.elasticsearch.common.geo.GeoBoundingBoxTests; -import org.elasticsearch.common.geo.GeoUtils; +import org.elasticsearch.geometry.Rectangle; +import org.elasticsearch.index.mapper.BinaryGeoShapeDocValuesField; import org.elasticsearch.index.mapper.GeoPointFieldMapper; import org.elasticsearch.index.mapper.GeoShapeFieldMapper; import org.elasticsearch.index.mapper.MappedFieldType; @@ -54,14 +57,13 @@ import java.util.Map; import java.util.Set; import java.util.function.Consumer; -import java.util.function.Function; +import static org.elasticsearch.common.geo.GeoTestUtils.triangleTreeReader; import static org.hamcrest.Matchers.equalTo; public abstract class GeoGridAggregatorTestCase extends AggregatorTestCase { private static final String FIELD_NAME = "location"; - protected static final double GEOHASH_TOLERANCE = 1E-5D; /** * Generate a random precision according to the rules of the given aggregation. @@ -73,6 +75,16 @@ public abstract class GeoGridAggregatorTestCase */ protected abstract String hashAsString(double lng, double lat, int precision); + /** + * Return a point within the bounds of the tile grid + */ + protected abstract Point randomPoint(); + + /** + * Return the bounding tile as a {@link Rectangle} for a given point + */ + protected abstract Rectangle getTile(double lng, double lat, int precision); + /** * Create a new named {@link GeoGridAggregationBuilder}-derived builder */ @@ -175,57 +187,55 @@ public void testGeoPointWithSeveralDocs() throws IOException { }, new GeoPointFieldMapper.GeoPointFieldType()); } - public void testBounds() throws IOException { - final int numDocs = randomIntBetween(64, 256); + public void testGeoPointBounds() throws IOException { + final int precision = randomPrecision(); + final int numDocs = randomIntBetween(100, 200); + int numDocsWithin = 0; final GeoGridAggregationBuilder builder = createBuilder("_name"); expectThrows(IllegalArgumentException.class, () -> builder.precision(-1)); expectThrows(IllegalArgumentException.class, () -> builder.precision(30)); - // only consider bounding boxes that are at least GEOHASH_TOLERANCE wide and have quantized coordinates - GeoBoundingBox bbox = randomValueOtherThanMany( - (b) -> Math.abs(GeoUtils.normalizeLon(b.right()) - GeoUtils.normalizeLon(b.left())) < GEOHASH_TOLERANCE, - GeoBoundingBoxTests::randomBBox); - Function encodeDecodeLat = (lat) -> GeoEncodingUtils.decodeLatitude(GeoEncodingUtils.encodeLatitude(lat)); - Function encodeDecodeLon = (lon) -> GeoEncodingUtils.decodeLongitude(GeoEncodingUtils.encodeLongitude(lon)); - bbox.topLeft().reset(encodeDecodeLat.apply(bbox.top()), encodeDecodeLon.apply(bbox.left())); - bbox.bottomRight().reset(encodeDecodeLat.apply(bbox.bottom()), encodeDecodeLon.apply(bbox.right())); + GeoBoundingBox bbox = GeoBoundingBoxTests.randomBBox(); + final double boundsTop = bbox.top(); + final double boundsBottom = bbox.bottom(); + final double boundsWestLeft; + final double boundsWestRight; + final double boundsEastLeft; + final double boundsEastRight; + final boolean crossesDateline; + if (bbox.right() < bbox.left()) { + boundsWestLeft = -180; + boundsWestRight = bbox.right(); + boundsEastLeft = bbox.left(); + boundsEastRight = 180; + crossesDateline = true; + } else { // only set east bounds + boundsEastLeft = bbox.left(); + boundsEastRight = bbox.right(); + boundsWestLeft = 0; + boundsWestRight = 0; + crossesDateline = false; + } - int in = 0, out = 0; List docs = new ArrayList<>(); - while (in + out < numDocs) { - if (bbox.left() > bbox.right()) { - if (randomBoolean()) { - double lonWithin = randomBoolean() ? - randomDoubleBetween(bbox.left(), 180.0, true) - : randomDoubleBetween(-180.0, bbox.right(), true); - double latWithin = randomDoubleBetween(bbox.bottom(), bbox.top(), true); - in++; - docs.add(new LatLonDocValuesField(FIELD_NAME, latWithin, lonWithin)); - } else { - double lonOutside = randomDoubleBetween(bbox.left(), bbox.right(), true); - double latOutside = randomDoubleBetween(bbox.top(), -90, false); - out++; - docs.add(new LatLonDocValuesField(FIELD_NAME, latOutside, lonOutside)); - } - } else { - if (randomBoolean()) { - double lonWithin = randomDoubleBetween(bbox.left(), bbox.right(), true); - double latWithin = randomDoubleBetween(bbox.bottom(), bbox.top(), true); - in++; - docs.add(new LatLonDocValuesField(FIELD_NAME, latWithin, lonWithin)); - } else { - double lonOutside = GeoUtils.normalizeLon(randomDoubleBetween(bbox.right(), 180.001, false)); - double latOutside = GeoUtils.normalizeLat(randomDoubleBetween(bbox.top(), 90.001, false)); - out++; - docs.add(new LatLonDocValuesField(FIELD_NAME, latOutside, lonOutside)); - } + for (int i = 0; i < numDocs; i++) { + Point p; + p = randomPoint(); + double x = GeoTestUtils.encodeDecodeLon(p.getX()); + double y = GeoTestUtils.encodeDecodeLat(p.getY()); + Rectangle pointTile = getTile(x, y, precision); + + boolean intersectsBounds = boundsTop >= pointTile.getMinY() && boundsBottom <= pointTile.getMaxY() + && (boundsEastLeft <= pointTile.getMaxX() && boundsEastRight >= pointTile.getMinX() + || (crossesDateline && boundsWestLeft <= pointTile.getMaxX() && boundsWestRight >= pointTile.getMinX())); + if (intersectsBounds) { + numDocsWithin += 1; } - + docs.add(new LatLonDocValuesField(FIELD_NAME, p.getLat(), p.getLon())); } - final long numDocsInBucket = in; - final int precision = randomPrecision(); + final long numDocsInBucket = numDocsWithin; testCase(new MatchAllDocsQuery(), FIELD_NAME, precision, bbox, iw -> { for (LatLonDocValuesField docField : docs) { @@ -242,6 +252,82 @@ public void testBounds() throws IOException { }, new GeoPointFieldMapper.GeoPointFieldType()); } + public void testGeoShapeBounds() throws IOException { + final int precision = randomPrecision(); + final int numDocs = randomIntBetween(100, 200); + int numDocsWithin = 0; + final GeoGridAggregationBuilder builder = createBuilder("_name"); + + expectThrows(IllegalArgumentException.class, () -> builder.precision(-1)); + expectThrows(IllegalArgumentException.class, () -> builder.precision(30)); + + GeoBoundingBox bbox = GeoBoundingBoxTests.randomBBox(); + final double boundsTop = bbox.top(); + final double boundsBottom = bbox.bottom(); + final double boundsWestLeft; + final double boundsWestRight; + final double boundsEastLeft; + final double boundsEastRight; + final boolean crossesDateline; + if (bbox.right() < bbox.left()) { + boundsWestLeft = -180; + boundsWestRight = bbox.right(); + boundsEastLeft = bbox.left(); + boundsEastRight = 180; + crossesDateline = true; + } else { // only set east bounds + boundsEastLeft = bbox.left(); + boundsEastRight = bbox.right(); + boundsWestLeft = 0; + boundsWestRight = 0; + crossesDateline = false; + } + + List docs = new ArrayList<>(); + List points = new ArrayList<>(); + for (int i = 0; i < numDocs; i++) { + Point p; + p = randomPoint(); + double x = GeoTestUtils.encodeDecodeLon(p.getX()); + double y = GeoTestUtils.encodeDecodeLat(p.getY()); + Rectangle pointTile = getTile(x, y, precision); + + + TriangleTreeReader reader = triangleTreeReader(p, GeoShapeCoordinateEncoder.INSTANCE); + GeoRelation tileRelation = reader.relateTile(GeoShapeCoordinateEncoder.INSTANCE.encodeX(pointTile.getMinX()), + GeoShapeCoordinateEncoder.INSTANCE.encodeY(pointTile.getMinY()), + GeoShapeCoordinateEncoder.INSTANCE.encodeX(pointTile.getMaxX()), + GeoShapeCoordinateEncoder.INSTANCE.encodeY(pointTile.getMaxY())); + boolean intersectsBounds = boundsTop >= pointTile.getMinY() && boundsBottom <= pointTile.getMaxY() + && (boundsEastLeft <= pointTile.getMaxX() && boundsEastRight >= pointTile.getMinX() + || (crossesDateline && boundsWestLeft <= pointTile.getMaxX() && boundsWestRight >= pointTile.getMinX())); + if (tileRelation != GeoRelation.QUERY_DISJOINT && intersectsBounds) { + numDocsWithin += 1; + } + + + points.add(p); + docs.add(new BinaryGeoShapeDocValuesField(FIELD_NAME, + GeoTestUtils.toDecodedTriangles(p), new CentroidCalculator(p))); + } + + final long numDocsInBucket = numDocsWithin; + + testCase(new MatchAllDocsQuery(), FIELD_NAME, precision, bbox, iw -> { + for (BinaryGeoShapeDocValuesField docField : docs) { + iw.addDocument(Collections.singletonList(docField)); + } + }, + geoGrid -> { + assertThat(AggregationInspectionHelper.hasValue(geoGrid), equalTo(numDocsInBucket > 0)); + long docCount = 0; + for (int i = 0; i < geoGrid.getBuckets().size(); i++) { + docCount += geoGrid.getBuckets().get(i).getDocCount(); + } + assertThat(docCount, equalTo(numDocsInBucket)); + }, new GeoShapeFieldMapper.GeoShapeFieldType()); + } + public void testGeoShapeWithSeveralDocs() throws IOException { int precision = randomIntBetween(1, 4); int numShapes = randomIntBetween(8, 128); diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoGridTilerTests.java b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoGridTilerTests.java index 8d3f8b04aa7d3..9c64dccfafff9 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoGridTilerTests.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoGridTilerTests.java @@ -43,13 +43,15 @@ import static org.elasticsearch.common.geo.GeoTestUtils.encodeDecodeLat; import static org.elasticsearch.common.geo.GeoTestUtils.encodeDecodeLon; import static org.elasticsearch.common.geo.GeoTestUtils.triangleTreeReader; +import static org.elasticsearch.search.aggregations.bucket.geogrid.GeoTileUtils.LATITUDE_MASK; import static org.elasticsearch.search.aggregations.bucket.geogrid.GeoTileUtils.NORMALIZED_LATITUDE_MASK; import static org.elasticsearch.search.aggregations.bucket.geogrid.GeoTileUtils.NORMALIZED_NEGATIVE_LATITUDE_MASK; + import static org.hamcrest.Matchers.equalTo; public class GeoGridTilerTests extends ESTestCase { - private static final GeoGridTiler.GeoTileGridTiler GEOTILE = GeoGridTiler.GeoTileGridTiler.INSTANCE; - private static final GeoGridTiler.GeoHashGridTiler GEOHASH = GeoGridTiler.GeoHashGridTiler.INSTANCE; + private static final GeoTileGridTiler GEOTILE = new GeoTileGridTiler(); + private static final GeoHashGridTiler GEOHASH = new GeoHashGridTiler(); public void testGeoTile() throws Exception { double x = randomDouble(); @@ -82,6 +84,65 @@ public void testGeoTile() throws Exception { } } + public void testAdvancePointValue() { + for (int i = 0; i < 100; i++) { + int precision = randomIntBetween(1, 6); + int size = randomIntBetween(1, 10); + long[] values = new long[size]; + int idx = randomIntBetween(0, size - 1); + Point point = GeometryTestUtils.randomPoint(false); + for (GeoGridTiler tiler : List.of(GEOTILE, GEOHASH)) { + int newIdx = tiler.advancePointValue(values, point.getX(), point.getY(), precision, idx); + assertThat(newIdx, equalTo(idx + 1)); + assertThat(values[idx], equalTo(tiler.encode(point.getX(), point.getY(), precision))); + } + } + } + + public void testBoundedGeotileAdvancePointValue() { + for (int i = 0; i < 100; i++) { + int precision = randomIntBetween(1, 6); + int size = randomIntBetween(1, 10); + long[] values = new long[size]; + int idx = randomIntBetween(0, size - 1); + Point point = GeometryTestUtils.randomPoint(false); + GeoBoundingBox geoBoundingBox = GeoBoundingBoxTests.randomBBox(); + + BoundedGeoTileGridTiler tiler = new BoundedGeoTileGridTiler(geoBoundingBox); + int newIdx = tiler.advancePointValue(values, point.getX(), point.getY(), precision, idx); + if (newIdx == idx + 1) { + assertTrue(tiler.cellIntersectsGeoBoundingBox(GeoTileUtils.toBoundingBox(values[idx]))); + assertThat(values[idx], equalTo(tiler.encode(point.getX(), point.getY(), precision))); + assertThat(newIdx, equalTo(idx + 1)); + } else { + assertThat(newIdx, equalTo(idx)); + assertThat(values[idx], equalTo(0L)); + } + } + } + + public void testBoundedGeohashAdvancePointValue() { + for (int i = 0; i < 100; i++) { + int precision = randomIntBetween(1, 6); + int size = randomIntBetween(1, 10); + long[] values = new long[size]; + int idx = randomIntBetween(0, size - 1); + Point point = GeometryTestUtils.randomPoint(false); + GeoBoundingBox geoBoundingBox = GeoBoundingBoxTests.randomBBox(); + + BoundedGeoHashGridTiler tiler = new BoundedGeoHashGridTiler(geoBoundingBox); + int newIdx = tiler.advancePointValue(values, point.getX(), point.getY(), precision, idx); + if (newIdx == idx + 1) { + assertTrue(tiler.cellIntersectsGeoBoundingBox(Geohash.toBoundingBox(Geohash.stringEncode(values[idx])))); + assertThat(values[idx], equalTo(tiler.encode(point.getX(), point.getY(), precision))); + assertThat(newIdx, equalTo(idx + 1)); + } else { + assertThat(newIdx, equalTo(idx)); + assertThat(values[idx], equalTo(0L)); + } + } + } + public void testGeoTileSetValuesBruteAndRecursiveMultiline() throws Exception { MultiLine geometry = GeometryTestUtils.randomMultiLine(false); checkGeoTileSetValuesBruteAndRecursive(geometry); @@ -100,61 +161,30 @@ public void testGeoTileSetValuesBruteAndRecursivePoints() throws Exception { checkGeoHashSetValuesBruteAndRecursive(geometry); } - private void checkGeoTileSetValuesBruteAndRecursive(Geometry geometry) throws Exception { - int precision = randomIntBetween(1, 5); - GeoShapeIndexer indexer = new GeoShapeIndexer(true, "test"); - geometry = indexer.prepareForIndexing(geometry); - TriangleTreeReader reader = triangleTreeReader(geometry, GeoShapeCoordinateEncoder.INSTANCE); - MultiGeoValues.GeoShapeValue value = new MultiGeoValues.GeoShapeValue(reader); - GeoShapeCellValues recursiveValues = new GeoShapeCellValues(null, precision, GEOTILE); - int recursiveCount; - { - recursiveCount = GEOTILE.setValuesByRasterization(0, 0, 0, recursiveValues, 0, precision, value); - } - GeoShapeCellValues bruteForceValues = new GeoShapeCellValues(null, precision, GEOTILE); - int bruteForceCount; - { - final double tiles = 1 << precision; - MultiGeoValues.BoundingBox bounds = value.boundingBox(); - int minXTile = GeoTileUtils.getXTile(bounds.minX(), (long) tiles); - int minYTile = GeoTileUtils.getYTile(bounds.maxY(), (long) tiles); - int maxXTile = GeoTileUtils.getXTile(bounds.maxX(), (long) tiles); - int maxYTile = GeoTileUtils.getYTile(bounds.minY(), (long) tiles); - bruteForceCount = GEOTILE.setValuesByBruteForceScan(bruteForceValues, value, precision, minXTile, minYTile, maxXTile, maxYTile); - } - assertThat(geometry.toString(), recursiveCount, equalTo(bruteForceCount)); - long[] recursive = Arrays.copyOf(recursiveValues.getValues(), recursiveCount); - long[] bruteForce = Arrays.copyOf(bruteForceValues.getValues(), bruteForceCount); - Arrays.sort(recursive); - Arrays.sort(bruteForce); - assertArrayEquals(geometry.toString(), recursive, bruteForce); - } + // tests that bounding boxes of shapes crossing the dateline are correctly wrapped + public void testGeoTileSetValuesBoundingBoxes_BoundedGeoShapeCellValues() throws Exception { + for (int i = 0; i < 1; i++) { + int precision = randomIntBetween(0, 4); + GeoShapeIndexer indexer = new GeoShapeIndexer(true, "test"); + Geometry geometry = indexer.prepareForIndexing(randomValueOtherThanMany(g -> { + try { + indexer.prepareForIndexing(g); + return false; + } catch (Exception e) { + return true; + } + }, () -> boxToGeo(GeoBoundingBoxTests.randomBBox()))); - private void checkGeoHashSetValuesBruteAndRecursive(Geometry geometry) throws Exception { - int precision = randomIntBetween(1, 3); - GeoShapeIndexer indexer = new GeoShapeIndexer(true, "test"); - geometry = indexer.prepareForIndexing(geometry); - TriangleTreeReader reader = triangleTreeReader(geometry, GeoShapeCoordinateEncoder.INSTANCE); - MultiGeoValues.GeoShapeValue value = new MultiGeoValues.GeoShapeValue(reader); - GeoShapeCellValues recursiveValues = new GeoShapeCellValues(null, precision, GEOHASH); - int recursiveCount; - { - recursiveCount = GEOHASH.setValuesByRasterization("", recursiveValues, 0, precision, value); - } - GeoShapeCellValues bruteForceValues = new GeoShapeCellValues(null, precision, GEOHASH); - int bruteForceCount; - { - MultiGeoValues.BoundingBox bounds = value.boundingBox(); - bruteForceCount = GEOHASH.setValuesByBruteForceScan(bruteForceValues, value, precision, bounds); - } + TriangleTreeReader reader = triangleTreeReader(geometry, GeoShapeCoordinateEncoder.INSTANCE); + GeoBoundingBox geoBoundingBox = GeoBoundingBoxTests.randomBBox(); + MultiGeoValues.GeoShapeValue value = new MultiGeoValues.GeoShapeValue(reader); + GeoShapeCellValues cellValues = new GeoShapeCellValues(null, precision, GEOTILE); - assertThat(geometry.toString(), recursiveCount, equalTo(bruteForceCount)); + int numTiles = new BoundedGeoTileGridTiler(geoBoundingBox).setValues(cellValues, value, precision); + int expected = numTiles(value, precision, geoBoundingBox); - long[] recursive = Arrays.copyOf(recursiveValues.getValues(), recursiveCount); - long[] bruteForce = Arrays.copyOf(bruteForceValues.getValues(), bruteForceCount); - Arrays.sort(recursive); - Arrays.sort(bruteForce); - assertArrayEquals(geometry.toString(), recursive, bruteForce); + assertThat(numTiles, equalTo(expected)); + } } // test random rectangles that can cross the date-line and verify that there are an expected @@ -174,7 +204,7 @@ public void testGeoTileSetValuesBoundingBoxes_UnboundedGeoShapeCellValues() thro TriangleTreeReader reader = triangleTreeReader(geometry, GeoShapeCoordinateEncoder.INSTANCE); MultiGeoValues.GeoShapeValue value = new MultiGeoValues.GeoShapeValue(reader); - GeoShapeCellValues unboundedCellValues = new GeoShapeCellValues(null, precision, GEOTILE); + CellValues unboundedCellValues = new GeoShapeCellValues(null, precision, GEOTILE); int numTiles = GEOTILE.setValues(unboundedCellValues, value, precision); int expected = numTiles(value, precision); assertThat(numTiles, equalTo(expected)); @@ -202,6 +232,9 @@ public void testTilerMatchPoint() throws Exception { }; for (Point point : pointCorners) { + if (point.getX() == GeoUtils.MAX_LON || point.getY() == -LATITUDE_MASK) { + continue; + } TriangleTreeReader reader = triangleTreeReader(point, GeoShapeCoordinateEncoder.INSTANCE); MultiGeoValues.GeoShapeValue value = new MultiGeoValues.GeoShapeValue(reader); GeoShapeCellValues unboundedCellValues = new GeoShapeCellValues(null, precision, GEOTILE); @@ -216,7 +249,7 @@ public void testTilerMatchPoint() throws Exception { public void testGeoHash() throws Exception { double x = randomDouble(); double y = randomDouble(); - int precision = randomIntBetween(0, Geohash.PRECISION); + int precision = randomIntBetween(0, 6); assertThat(GEOHASH.encode(x, y, precision), equalTo(Geohash.longEncode(x, y, precision))); Rectangle tile = Geohash.toBoundingBox(Geohash.stringEncode(x, y, 5)); @@ -228,23 +261,175 @@ public void testGeoHash() throws Exception { // test shape within tile bounds { - GeoShapeCellValues values = new GeoShapeCellValues(null, precision, GEOTILE); + GeoShapeCellValues values = new GeoShapeCellValues(null, precision, GEOHASH); int count = GEOHASH.setValues(values, value, 5); assertThat(count, equalTo(1)); } { - GeoShapeCellValues values = new GeoShapeCellValues(null, precision, GEOTILE); + GeoShapeCellValues values = new GeoShapeCellValues(null, precision, GEOHASH); int count = GEOHASH.setValues(values, value, 6); assertThat(count, equalTo(32)); } { - GeoShapeCellValues values = new GeoShapeCellValues(null, precision, GEOTILE); + GeoShapeCellValues values = new GeoShapeCellValues(null, precision, GEOHASH); int count = GEOHASH.setValues(values, value, 7); assertThat(count, equalTo(1024)); } } - private Geometry boxToGeo(GeoBoundingBox geoBox) { + private boolean tileIntersectsBounds(int x, int y, int precision, GeoBoundingBox bounds) { + if (bounds == null) { + return true; + } + final double boundsWestLeft; + final double boundsWestRight; + final double boundsEastLeft; + final double boundsEastRight; + final boolean crossesDateline; + if (bounds.right() < bounds.left()) { + boundsWestLeft = -180; + boundsWestRight = bounds.right(); + boundsEastLeft = bounds.left(); + boundsEastRight = 180; + crossesDateline = true; + } else { + boundsEastLeft = bounds.left(); + boundsEastRight = bounds.right(); + boundsWestLeft = 0; + boundsWestRight = 0; + crossesDateline = false; + } + + Rectangle tile = GeoTileUtils.toBoundingBox(x, y, precision); + + return (bounds.top() >= tile.getMinY() && bounds.bottom() <= tile.getMaxY() + && (boundsEastLeft <= tile.getMaxX() && boundsEastRight >= tile.getMinX() + || (crossesDateline && boundsWestLeft <= tile.getMaxX() && boundsWestRight >= tile.getMinX()))); + } + + private int numTiles(MultiGeoValues.GeoValue geoValue, int precision, GeoBoundingBox geoBox) throws Exception { + MultiGeoValues.BoundingBox bounds = geoValue.boundingBox(); + int count = 0; + + if (precision == 0) { + return 1; + } else if ((bounds.top > LATITUDE_MASK && bounds.bottom > LATITUDE_MASK) + || (bounds.top < -LATITUDE_MASK && bounds.bottom < -LATITUDE_MASK)) { + return 0; + } + final double tiles = 1 << precision; + int minYTile = GeoTileUtils.getYTile(bounds.maxY(), (long) tiles); + int maxYTile = GeoTileUtils.getYTile(bounds.minY(), (long) tiles); + if ((bounds.posLeft >= 0 && bounds.posRight >= 0) && (bounds.negLeft < 0 && bounds.negRight < 0)) { + // box one + int minXTileNeg = GeoTileUtils.getXTile(bounds.negLeft, (long) tiles); + int maxXTileNeg = GeoTileUtils.getXTile(bounds.negRight, (long) tiles); + + for (int x = minXTileNeg; x <= maxXTileNeg; x++) { + for (int y = minYTile; y <= maxYTile; y++) { + Rectangle r = GeoTileUtils.toBoundingBox(x, y, precision); + if (tileIntersectsBounds(x, y, precision, geoBox) && geoValue.relate(r) != GeoRelation.QUERY_DISJOINT) { + count += 1; + } + } + } + + // box two + int minXTilePos = GeoTileUtils.getXTile(bounds.posLeft, (long) tiles); + if (minXTilePos > maxXTileNeg + 1) { + minXTilePos -= 1; + } + + int maxXTilePos = GeoTileUtils.getXTile(bounds.posRight, (long) tiles); + + for (int x = minXTilePos; x <= maxXTilePos; x++) { + for (int y = minYTile; y <= maxYTile; y++) { + Rectangle r = GeoTileUtils.toBoundingBox(x, y, precision); + if (tileIntersectsBounds(x, y, precision, geoBox) && geoValue.relate(r) != GeoRelation.QUERY_DISJOINT) { + count += 1; + } + } + } + return count; + } else { + int minXTile = GeoTileUtils.getXTile(bounds.minX(), (long) tiles); + int maxXTile = GeoTileUtils.getXTile(bounds.maxX(), (long) tiles); + + if (minXTile == maxXTile && minYTile == maxYTile) { + return tileIntersectsBounds(minXTile, minYTile, precision, geoBox) ? 1 : 0; + } + + for (int x = minXTile; x <= maxXTile; x++) { + for (int y = minYTile; y <= maxYTile; y++) { + Rectangle r = GeoTileUtils.toBoundingBox(x, y, precision); + if (tileIntersectsBounds(x, y, precision, geoBox) && geoValue.relate(r) != GeoRelation.QUERY_DISJOINT) { + count += 1; + } + } + } + return count; + } + } + + private void checkGeoTileSetValuesBruteAndRecursive(Geometry geometry) throws Exception { + int precision = randomIntBetween(1, 4); + GeoShapeIndexer indexer = new GeoShapeIndexer(true, "test"); + geometry = indexer.prepareForIndexing(geometry); + TriangleTreeReader reader = triangleTreeReader(geometry, GeoShapeCoordinateEncoder.INSTANCE); + MultiGeoValues.GeoShapeValue value = new MultiGeoValues.GeoShapeValue(reader); + GeoShapeCellValues recursiveValues = new GeoShapeCellValues(null, precision, GEOTILE); + int recursiveCount; + { + recursiveCount = GEOTILE.setValuesByRasterization(0, 0, 0, recursiveValues, 0, precision, value); + } + GeoShapeCellValues bruteForceValues = new GeoShapeCellValues(null, precision, GEOTILE); + int bruteForceCount; + { + final double tiles = 1 << precision; + MultiGeoValues.BoundingBox bounds = value.boundingBox(); + int minXTile = GeoTileUtils.getXTile(bounds.minX(), (long) tiles); + int minYTile = GeoTileUtils.getYTile(bounds.maxY(), (long) tiles); + int maxXTile = GeoTileUtils.getXTile(bounds.maxX(), (long) tiles); + int maxYTile = GeoTileUtils.getYTile(bounds.minY(), (long) tiles); + bruteForceCount = GEOTILE.setValuesByBruteForceScan(bruteForceValues, value, precision, minXTile, minYTile, maxXTile, maxYTile); + } + assertThat(geometry.toString(), recursiveCount, equalTo(bruteForceCount)); + long[] recursive = Arrays.copyOf(recursiveValues.getValues(), recursiveCount); + long[] bruteForce = Arrays.copyOf(bruteForceValues.getValues(), bruteForceCount); + Arrays.sort(recursive); + Arrays.sort(bruteForce); + assertArrayEquals(geometry.toString(), recursive, bruteForce); + } + + private void checkGeoHashSetValuesBruteAndRecursive(Geometry geometry) throws Exception { + int precision = randomIntBetween(1, 3); + GeoShapeIndexer indexer = new GeoShapeIndexer(true, "test"); + geometry = indexer.prepareForIndexing(geometry); + TriangleTreeReader reader = triangleTreeReader(geometry, GeoShapeCoordinateEncoder.INSTANCE); + MultiGeoValues.GeoShapeValue value = new MultiGeoValues.GeoShapeValue(reader); + GeoShapeCellValues recursiveValues = new GeoShapeCellValues(null, precision, GEOHASH); + int recursiveCount; + { + recursiveCount = GEOHASH.setValuesByRasterization("", recursiveValues, 0, precision, value); + } + GeoShapeCellValues bruteForceValues = new GeoShapeCellValues(null, precision, GEOHASH); + int bruteForceCount; + { + MultiGeoValues.BoundingBox bounds = value.boundingBox(); + bruteForceCount = GEOHASH.setValuesByBruteForceScan(bruteForceValues, value, precision, bounds); + } + + assertThat(geometry.toString(), recursiveCount, equalTo(bruteForceCount)); + + long[] recursive = Arrays.copyOf(recursiveValues.getValues(), recursiveCount); + long[] bruteForce = Arrays.copyOf(bruteForceValues.getValues(), bruteForceCount); + Arrays.sort(recursive); + Arrays.sort(bruteForce); + assertArrayEquals(geometry.toString(), recursive, bruteForce); + } + + + static Geometry boxToGeo(GeoBoundingBox geoBox) { // turn into polygon if (geoBox.right() < geoBox.left() && geoBox.right() != -180) { return new MultiPolygon(List.of( @@ -313,6 +498,11 @@ private int numTiles(MultiGeoValues.GeoValue geoValue, int precision) { } else { int minXTile = GeoTileUtils.getXTile(bounds.minX(), (long) tiles); int maxXTile = GeoTileUtils.getXTile(bounds.maxX(), (long) tiles); + + if (minXTile == maxXTile && minYTile == maxYTile) { + return 1; + } + for (int x = minXTile; x <= maxXTile; x++) { for (int y = minYTile; y <= maxYTile; y++) { Rectangle r = GeoTileUtils.toBoundingBox(x, y, precision); diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoHashGridAggregatorTests.java b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoHashGridAggregatorTests.java index 7fa517807f619..2aab993c1d8c0 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoHashGridAggregatorTests.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoHashGridAggregatorTests.java @@ -19,6 +19,11 @@ package org.elasticsearch.search.aggregations.bucket.geogrid; +import org.elasticsearch.geo.GeometryTestUtils; +import org.elasticsearch.geometry.Point; +import org.elasticsearch.geometry.Rectangle; +import org.elasticsearch.geometry.utils.Geohash; + import static org.elasticsearch.geometry.utils.Geohash.stringEncode; public class GeoHashGridAggregatorTests extends GeoGridAggregatorTestCase { @@ -33,6 +38,16 @@ protected String hashAsString(double lng, double lat, int precision) { return stringEncode(lng, lat, precision); } + @Override + protected Point randomPoint() { + return GeometryTestUtils.randomPoint(false); + } + + @Override + protected Rectangle getTile(double lng, double lat, int precision) { + return Geohash.toBoundingBox(Geohash.stringEncode(lng, lat, precision)); + } + @Override protected GeoGridAggregationBuilder createBuilder(String name) { return new GeoHashGridAggregationBuilder(name); diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoTileGridAggregatorTests.java b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoTileGridAggregatorTests.java index 85b2306403230..fd4c52a0f7d8d 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoTileGridAggregatorTests.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoTileGridAggregatorTests.java @@ -19,6 +19,10 @@ package org.elasticsearch.search.aggregations.bucket.geogrid; +import org.elasticsearch.common.geo.GeoUtils; +import org.elasticsearch.geometry.Point; +import org.elasticsearch.geometry.Rectangle; + public class GeoTileGridAggregatorTests extends GeoGridAggregatorTestCase { @Override @@ -31,6 +35,17 @@ protected String hashAsString(double lng, double lat, int precision) { return GeoTileUtils.stringEncode(GeoTileUtils.longEncode(lng, lat, precision)); } + @Override + protected Point randomPoint() { + return new Point(randomDoubleBetween(GeoUtils.MIN_LON, GeoUtils.MAX_LON, true), + randomDoubleBetween(-GeoTileUtils.LATITUDE_MASK, GeoTileUtils.LATITUDE_MASK, false)); + } + + @Override + protected Rectangle getTile(double lng, double lat, int precision) { + return GeoTileUtils.toBoundingBox(GeoTileUtils.longEncode(lng, lat, precision)); + } + @Override protected GeoGridAggregationBuilder createBuilder(String name) { return new GeoTileGridAggregationBuilder(name);