mirror of
https://github.com/elastic/elasticsearch.git
synced 2025-04-24 15:17:30 -04:00
ESQL: Support ST_EXTENT_AGG (#117451)
This PR adds support for ST_EXTENT_AGG aggregation, i.e., computing a bounding box over a set of points/shapes (Cartesian or geo). Note the difference between this aggregation and the already implemented scalar function ST_EXTENT. This isn't a very efficient implementation, and future PRs will attempt to read these extents directly from the doc values. We currently always use longitude wrapping, i.e., we may wrap around the dateline for a smaller bounding box. Future PRs will let the user control this behavior. Fixes #104659.
This commit is contained in:
parent
140d88c59a
commit
2be4cd983f
79 changed files with 4926 additions and 237 deletions
|
@ -83,10 +83,15 @@ public class SpatialEnvelopeVisitor implements GeometryVisitor<Boolean, RuntimeE
|
|||
return Optional.empty();
|
||||
}
|
||||
|
||||
public enum WrapLongitude {
|
||||
NO_WRAP,
|
||||
WRAP
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the BBOX assuming the CRS is geographic (eg WGS84) and optionally wrapping the longitude around the dateline.
|
||||
*/
|
||||
public static Optional<Rectangle> visitGeo(Geometry geometry, boolean wrapLongitude) {
|
||||
public static Optional<Rectangle> visitGeo(Geometry geometry, WrapLongitude wrapLongitude) {
|
||||
var visitor = new SpatialEnvelopeVisitor(new GeoPointVisitor(wrapLongitude));
|
||||
if (geometry.visit(visitor)) {
|
||||
return Optional.of(visitor.getResult());
|
||||
|
@ -181,40 +186,16 @@ public class SpatialEnvelopeVisitor implements GeometryVisitor<Boolean, RuntimeE
|
|||
* </ul>
|
||||
*/
|
||||
public static class GeoPointVisitor implements PointVisitor {
|
||||
private double minY = Double.POSITIVE_INFINITY;
|
||||
private double maxY = Double.NEGATIVE_INFINITY;
|
||||
private double minNegX = Double.POSITIVE_INFINITY;
|
||||
private double maxNegX = Double.NEGATIVE_INFINITY;
|
||||
private double minPosX = Double.POSITIVE_INFINITY;
|
||||
private double maxPosX = Double.NEGATIVE_INFINITY;
|
||||
protected double minY = Double.POSITIVE_INFINITY;
|
||||
protected double maxY = Double.NEGATIVE_INFINITY;
|
||||
protected double minNegX = Double.POSITIVE_INFINITY;
|
||||
protected double maxNegX = Double.NEGATIVE_INFINITY;
|
||||
protected double minPosX = Double.POSITIVE_INFINITY;
|
||||
protected double maxPosX = Double.NEGATIVE_INFINITY;
|
||||
|
||||
public double getMinY() {
|
||||
return minY;
|
||||
}
|
||||
private final WrapLongitude wrapLongitude;
|
||||
|
||||
public double getMaxY() {
|
||||
return maxY;
|
||||
}
|
||||
|
||||
public double getMinNegX() {
|
||||
return minNegX;
|
||||
}
|
||||
|
||||
public double getMaxNegX() {
|
||||
return maxNegX;
|
||||
}
|
||||
|
||||
public double getMinPosX() {
|
||||
return minPosX;
|
||||
}
|
||||
|
||||
public double getMaxPosX() {
|
||||
return maxPosX;
|
||||
}
|
||||
|
||||
private final boolean wrapLongitude;
|
||||
|
||||
public GeoPointVisitor(boolean wrapLongitude) {
|
||||
public GeoPointVisitor(WrapLongitude wrapLongitude) {
|
||||
this.wrapLongitude = wrapLongitude;
|
||||
}
|
||||
|
||||
|
@ -253,32 +234,35 @@ public class SpatialEnvelopeVisitor implements GeometryVisitor<Boolean, RuntimeE
|
|||
return getResult(minNegX, minPosX, maxNegX, maxPosX, maxY, minY, wrapLongitude);
|
||||
}
|
||||
|
||||
private static Rectangle getResult(
|
||||
protected static Rectangle getResult(
|
||||
double minNegX,
|
||||
double minPosX,
|
||||
double maxNegX,
|
||||
double maxPosX,
|
||||
double maxY,
|
||||
double minY,
|
||||
boolean wrapLongitude
|
||||
WrapLongitude wrapLongitude
|
||||
) {
|
||||
assert Double.isFinite(maxY);
|
||||
if (Double.isInfinite(minPosX)) {
|
||||
return new Rectangle(minNegX, maxNegX, maxY, minY);
|
||||
} else if (Double.isInfinite(minNegX)) {
|
||||
return new Rectangle(minPosX, maxPosX, maxY, minY);
|
||||
} else if (wrapLongitude) {
|
||||
double unwrappedWidth = maxPosX - minNegX;
|
||||
double wrappedWidth = (180 - minPosX) - (-180 - maxNegX);
|
||||
if (unwrappedWidth <= wrappedWidth) {
|
||||
return new Rectangle(minNegX, maxPosX, maxY, minY);
|
||||
} else {
|
||||
return new Rectangle(minPosX, maxNegX, maxY, minY);
|
||||
}
|
||||
} else {
|
||||
return new Rectangle(minNegX, maxPosX, maxY, minY);
|
||||
return switch (wrapLongitude) {
|
||||
case NO_WRAP -> new Rectangle(minNegX, maxPosX, maxY, minY);
|
||||
case WRAP -> maybeWrap(minNegX, minPosX, maxNegX, maxPosX, maxY, minY);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static Rectangle maybeWrap(double minNegX, double minPosX, double maxNegX, double maxPosX, double maxY, double minY) {
|
||||
double unwrappedWidth = maxPosX - minNegX;
|
||||
double wrappedWidth = 360 + maxNegX - minPosX;
|
||||
return unwrappedWidth <= wrappedWidth
|
||||
? new Rectangle(minNegX, maxPosX, maxY, minY)
|
||||
: new Rectangle(minPosX, maxNegX, maxY, minY);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isValid() {
|
||||
|
|
|
@ -13,6 +13,7 @@ import org.elasticsearch.geo.GeometryTestUtils;
|
|||
import org.elasticsearch.geo.ShapeTestUtils;
|
||||
import org.elasticsearch.geometry.Point;
|
||||
import org.elasticsearch.geometry.Rectangle;
|
||||
import org.elasticsearch.geometry.utils.SpatialEnvelopeVisitor.WrapLongitude;
|
||||
import org.elasticsearch.test.ESTestCase;
|
||||
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
|
@ -36,7 +37,7 @@ public class SpatialEnvelopeVisitorTests extends ESTestCase {
|
|||
public void testVisitGeoShapeNoWrap() {
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
var geometry = GeometryTestUtils.randomGeometryWithoutCircle(0, false);
|
||||
var bbox = SpatialEnvelopeVisitor.visitGeo(geometry, false);
|
||||
var bbox = SpatialEnvelopeVisitor.visitGeo(geometry, WrapLongitude.NO_WRAP);
|
||||
assertNotNull(bbox);
|
||||
assertTrue(i + ": " + geometry, bbox.isPresent());
|
||||
var result = bbox.get();
|
||||
|
@ -48,7 +49,8 @@ public class SpatialEnvelopeVisitorTests extends ESTestCase {
|
|||
public void testVisitGeoShapeWrap() {
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
var geometry = GeometryTestUtils.randomGeometryWithoutCircle(0, true);
|
||||
var bbox = SpatialEnvelopeVisitor.visitGeo(geometry, false);
|
||||
// TODO this should be WRAP instead
|
||||
var bbox = SpatialEnvelopeVisitor.visitGeo(geometry, WrapLongitude.NO_WRAP);
|
||||
assertNotNull(bbox);
|
||||
assertTrue(i + ": " + geometry, bbox.isPresent());
|
||||
var result = bbox.get();
|
||||
|
@ -81,7 +83,7 @@ public class SpatialEnvelopeVisitorTests extends ESTestCase {
|
|||
}
|
||||
|
||||
public void testVisitGeoPointsNoWrapping() {
|
||||
var visitor = new SpatialEnvelopeVisitor(new SpatialEnvelopeVisitor.GeoPointVisitor(false));
|
||||
var visitor = new SpatialEnvelopeVisitor(new SpatialEnvelopeVisitor.GeoPointVisitor(WrapLongitude.NO_WRAP));
|
||||
double minY = Double.MAX_VALUE;
|
||||
double maxY = -Double.MAX_VALUE;
|
||||
double minX = Double.MAX_VALUE;
|
||||
|
@ -103,7 +105,7 @@ public class SpatialEnvelopeVisitorTests extends ESTestCase {
|
|||
}
|
||||
|
||||
public void testVisitGeoPointsWrapping() {
|
||||
var visitor = new SpatialEnvelopeVisitor(new SpatialEnvelopeVisitor.GeoPointVisitor(true));
|
||||
var visitor = new SpatialEnvelopeVisitor(new SpatialEnvelopeVisitor.GeoPointVisitor(WrapLongitude.WRAP));
|
||||
double minY = Double.POSITIVE_INFINITY;
|
||||
double maxY = Double.NEGATIVE_INFINITY;
|
||||
double minNegX = Double.POSITIVE_INFINITY;
|
||||
|
@ -145,7 +147,7 @@ public class SpatialEnvelopeVisitorTests extends ESTestCase {
|
|||
}
|
||||
|
||||
public void testWillCrossDateline() {
|
||||
var visitor = new SpatialEnvelopeVisitor(new SpatialEnvelopeVisitor.GeoPointVisitor(true));
|
||||
var visitor = new SpatialEnvelopeVisitor(new SpatialEnvelopeVisitor.GeoPointVisitor(WrapLongitude.WRAP));
|
||||
visitor.visit(new Point(-90.0, 0.0));
|
||||
visitor.visit(new Point(90.0, 0.0));
|
||||
assertCrossesDateline(visitor, false);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue