Skip to content

Commit

Permalink
Added tests for SpatialEnvelopeVisitor
Browse files Browse the repository at this point in the history
Asserting in particular that the geo-wrapping over the dateline works.
  • Loading branch information
craigtaverner committed Nov 25, 2024
1 parent 0bcdea3 commit 2f17bcc
Show file tree
Hide file tree
Showing 2 changed files with 199 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public SpatialEnvelopeVisitor(PointVisitor pointVisitor) {

/**
* Determine the BBOX without considering the CRS or wrapping of the longitude.
* Note that incoming BBOX's that do cross the dateline (minx>maxx) will be treated as invalid.
*/
public static Optional<Rectangle> visit(Geometry geometry) {
var visitor = new SpatialEnvelopeVisitor(new CartesianPointVisitor());
Expand Down Expand Up @@ -66,7 +67,7 @@ private interface PointVisitor {
Rectangle getResult();
}

private static class CartesianPointVisitor implements PointVisitor {
static class CartesianPointVisitor implements PointVisitor {
private double minX = Double.POSITIVE_INFINITY;
private double minY = Double.POSITIVE_INFINITY;
private double maxX = Double.NEGATIVE_INFINITY;
Expand All @@ -82,6 +83,9 @@ public void visitPoint(double x, double y) {

@Override
public void visitRectangle(double minX, double maxX, double maxY, double minY) {
if (minX > maxX) {
throw new IllegalArgumentException("Invalid cartesian rectangle: minX > maxX");
}
this.minX = Math.min(this.minX, minX);
this.minY = Math.min(this.minY, minY);
this.maxX = Math.max(this.maxX, maxX);
Expand All @@ -99,7 +103,7 @@ public Rectangle getResult() {
}
}

private static class GeoPointVisitor implements PointVisitor {
static class GeoPointVisitor implements PointVisitor {
private double minY = Double.POSITIVE_INFINITY;
private double maxY = Double.NEGATIVE_INFINITY;
private double minNegX = Double.POSITIVE_INFINITY;
Expand All @@ -109,7 +113,7 @@ private static class GeoPointVisitor implements PointVisitor {

private final boolean wrapLongitude;

private GeoPointVisitor(boolean wrapLongitude) {
GeoPointVisitor(boolean wrapLongitude) {
this.wrapLongitude = wrapLongitude;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

package org.elasticsearch.xpack.esql.core.util;

import org.elasticsearch.geo.GeometryTestUtils;
import org.elasticsearch.geo.ShapeTestUtils;
import org.elasticsearch.geometry.Point;
import org.elasticsearch.geometry.Rectangle;
import org.elasticsearch.test.ESTestCase;

import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.lessThanOrEqualTo;

public class SpatialEnvelopeVisitorTests extends ESTestCase {

public void testVisitCartesianShape() {
for (int i = 0; i < 1000; i++) {
var geometry = ShapeTestUtils.randomGeometryWithoutCircle(0, false);
var bbox = SpatialEnvelopeVisitor.visit(geometry);
assertNotNull(bbox);
assertTrue(i + ": " + geometry, bbox.isPresent());
var result = bbox.get();
assertThat(i + ": " + geometry, result.getMinX(), lessThanOrEqualTo(result.getMaxX()));
assertThat(i + ": " + geometry, result.getMinY(), lessThanOrEqualTo(result.getMaxY()));
}
}

public void testVisitGeoShapeNoWrap() {
for (int i = 0; i < 1000; i++) {
var geometry = GeometryTestUtils.randomGeometryWithoutCircle(0, false);
var bbox = SpatialEnvelopeVisitor.visit(geometry, false);
assertNotNull(bbox);
assertTrue(i + ": " + geometry, bbox.isPresent());
var result = bbox.get();
assertThat(i + ": " + geometry, result.getMinX(), lessThanOrEqualTo(result.getMaxX()));
assertThat(i + ": " + geometry, result.getMinY(), lessThanOrEqualTo(result.getMaxY()));
}
}

public void testVisitGeoShapeWrap() {
for (int i = 0; i < 1000; i++) {
var geometry = GeometryTestUtils.randomGeometryWithoutCircle(0, true);
var bbox = SpatialEnvelopeVisitor.visit(geometry, false);
assertNotNull(bbox);
assertTrue(i + ": " + geometry, bbox.isPresent());
var result = bbox.get();
assertThat(i + ": " + geometry, result.getMinX(), lessThanOrEqualTo(result.getMaxX()));
assertThat(i + ": " + geometry, result.getMinY(), lessThanOrEqualTo(result.getMaxY()));
}
}

public void testVisitCartesianPoints() {
var visitor = new SpatialEnvelopeVisitor(new SpatialEnvelopeVisitor.CartesianPointVisitor());
double minX = Double.MAX_VALUE;
double minY = Double.MAX_VALUE;
double maxX = -Double.MAX_VALUE;
double maxY = -Double.MAX_VALUE;
for (int i = 0; i < 1000; i++) {
var x = randomFloat();
var y = randomFloat();
var point = new Point(x, y);
visitor.visit(point);
minX = Math.min(minX, x);
minY = Math.min(minY, y);
maxX = Math.max(maxX, x);
maxY = Math.max(maxY, y);
var result = visitor.getResult();
assertThat(i + ": " + point, result.getMinX(), equalTo(minX));
assertThat(i + ": " + point, result.getMinY(), equalTo(minY));
assertThat(i + ": " + point, result.getMaxX(), equalTo(maxX));
assertThat(i + ": " + point, result.getMaxY(), equalTo(maxY));
}
}

public void testVisitGeoPointsNoWrapping() {
var visitor = new SpatialEnvelopeVisitor(new SpatialEnvelopeVisitor.GeoPointVisitor(false));
double minY = Double.MAX_VALUE;
double maxY = -Double.MAX_VALUE;
double minX = Double.MAX_VALUE;
double maxX = -Double.MAX_VALUE;
for (int i = 0; i < 1000; i++) {
var point = GeometryTestUtils.randomPoint();
visitor.visit(point);
minY = Math.min(minY, point.getY());
maxY = Math.max(maxY, point.getY());
minX = Math.min(minX, point.getX());
maxX = Math.max(maxX, point.getX());
var result = visitor.getResult();
assertThat(i + ": " + point, result.getMinX(), lessThanOrEqualTo(result.getMaxX()));
assertThat(i + ": " + point, result.getMinX(), equalTo(minX));
assertThat(i + ": " + point, result.getMinY(), equalTo(minY));
assertThat(i + ": " + point, result.getMaxX(), equalTo(maxX));
assertThat(i + ": " + point, result.getMaxY(), equalTo(maxY));
}
}

public void testVisitGeoPointsWrapping() {
var visitor = new SpatialEnvelopeVisitor(new SpatialEnvelopeVisitor.GeoPointVisitor(true));
double minY = Double.POSITIVE_INFINITY;
double maxY = Double.NEGATIVE_INFINITY;
double minNegX = Double.POSITIVE_INFINITY;
double maxNegX = Double.NEGATIVE_INFINITY;
double minPosX = Double.POSITIVE_INFINITY;
double maxPosX = Double.NEGATIVE_INFINITY;
for (int i = 0; i < 1000; i++) {
var point = GeometryTestUtils.randomPoint();
visitor.visit(point);
minY = Math.min(minY, point.getY());
maxY = Math.max(maxY, point.getY());
if (point.getX() >= 0) {
minPosX = Math.min(minPosX, point.getX());
maxPosX = Math.max(maxPosX, point.getX());
} else {
minNegX = Math.min(minNegX, point.getX());
maxNegX = Math.max(maxNegX, point.getX());
}
var result = visitor.getResult();
if (Double.isInfinite(minPosX)) {
// Only negative x values were considered
assertRectangleResult(i + ": " + point, result, minNegX, maxNegX, maxY, minY, false);
} else if (Double.isInfinite(minNegX)) {
// Only positive x values were considered
assertRectangleResult(i + ": " + point, result, minPosX, maxPosX, maxY, minY, false);
} else {
// Both positive and negative x values exist, we need to decide which way to wrap the bbox
double unwrappedWidth = maxPosX - minNegX;
double wrappedWidth = (180 - minPosX) - (-180 - maxNegX);
if (unwrappedWidth <= wrappedWidth) {
// The smaller bbox is around the front of the planet, no dateline wrapping required
assertRectangleResult(i + ": " + point, result, minNegX, maxPosX, maxY, minY, false);
} else {
// The smaller bbox is around the back of the planet, dateline wrapping required (minx > maxx)
assertRectangleResult(i + ": " + point, result, minPosX, maxNegX, maxY, minY, true);
}
}
}
}

public void testWillCrossDateline() {
var visitor = new SpatialEnvelopeVisitor(new SpatialEnvelopeVisitor.GeoPointVisitor(true));
visitor.visit(new Point(-90.0, 0.0));
visitor.visit(new Point(90.0, 0.0));
assertCrossesDateline(visitor, false);
visitor.visit(new Point(-89.0, 0.0));
visitor.visit(new Point(89.0, 0.0));
assertCrossesDateline(visitor, false);
visitor.visit(new Point(-100.0, 0.0));
visitor.visit(new Point(100.0, 0.0));
assertCrossesDateline(visitor, true);
visitor.visit(new Point(-70.0, 0.0));
visitor.visit(new Point(70.0, 0.0));
assertCrossesDateline(visitor, false);
visitor.visit(new Point(-120.0, 0.0));
visitor.visit(new Point(120.0, 0.0));
assertCrossesDateline(visitor, true);
}

private void assertCrossesDateline(SpatialEnvelopeVisitor visitor, boolean crossesDateline) {
var result = visitor.getResult();
if (crossesDateline) {
assertThat("Crosses dateline, minx>maxx", result.getMinX(), greaterThanOrEqualTo(result.getMaxX()));
} else {
assertThat("Does not cross dateline, minx<maxx", result.getMinX(), lessThanOrEqualTo(result.getMaxX()));
}
}

private void assertRectangleResult(
String s,
Rectangle result,
double minX,
double maxX,
double maxY,
double minY,
boolean crossesDateline
) {
if (crossesDateline) {
assertThat(s, result.getMinX(), greaterThanOrEqualTo(result.getMaxX()));
} else {
assertThat(s, result.getMinX(), lessThanOrEqualTo(result.getMaxX()));
}
assertThat(s, result.getMinX(), equalTo(minX));
assertThat(s, result.getMaxX(), equalTo(maxX));
assertThat(s, result.getMaxY(), equalTo(maxY));
assertThat(s, result.getMinY(), equalTo(minY));
}
}

0 comments on commit 2f17bcc

Please sign in to comment.