Skip to content

Commit

Permalink
Merge pull request #97 from aodn/bugs/6119-cql-box-large-lat
Browse files Browse the repository at this point in the history
Bug fix on BBOX cross meridian
  • Loading branch information
utas-raymondng authored Nov 27, 2024
2 parents 7e7ad5c + 0bda578 commit d8444e8
Show file tree
Hide file tree
Showing 6 changed files with 1,110 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public ElasticsearchClient geoNetworkElasticsearchClient(RestClientTransport tra
public Search createElasticSearch(ElasticsearchClient client,
ObjectMapper mapper,
@Value("${elasticsearch.index.name}") String indexName,
@Value("${elasticsearch.index.pageSize:2500}") Integer pageSize,
@Value("${elasticsearch.index.pageSize:2200}") Integer pageSize,
@Value("${elasticsearch.search_as_you_type.size:10}") Integer searchAsYouTypeSize) {

return new ElasticSearch(client, mapper, indexName, pageSize, searchAsYouTypeSize);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import au.org.aodn.ogcapi.server.core.model.enumeration.CQLCrsType;
import au.org.aodn.ogcapi.server.core.model.enumeration.CQLFieldsInterface;
import au.org.aodn.ogcapi.server.core.util.GeometryUtils;
import co.elastic.clients.elasticsearch._types.TopLeftBottomRightGeoBounds;
import co.elastic.clients.elasticsearch._types.query_dsl.BoolQuery;
import org.geotools.filter.spatial.BBOXImpl;
import org.geotools.geometry.jts.JTS;
import org.geotools.geometry.jts.JTSFactoryFinder;
Expand All @@ -11,6 +13,7 @@
import org.geotools.referencing.CRS;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.MultiLineString;
import org.opengis.filter.FilterVisitor;
import org.opengis.filter.MultiValuedFilter;
import org.opengis.filter.expression.Expression;
Expand All @@ -23,6 +26,7 @@
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.NoSuchAuthorityCodeException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import java.util.List;

/**
* This class support both 2D or 3D query, but now we just implement 2D and support very limited operation for CQL
Expand All @@ -46,7 +50,7 @@ public BBoxImpl(Expression geometry,
this.create3DCQL(geometry, box3D, matchAction);
}
else {
this.create2DCQL(geometry, bounds, matchAction, enumType);
this.create2DCQL(geometry, List.of(bounds), matchAction, enumType);
}
}

Expand Down Expand Up @@ -86,8 +90,13 @@ public BBoxImpl(
} else {
crs = null;
}
// This record the bounding box only, since the box may cross meridian, we need to split the polygon
this.bounds = new ReferencedEnvelope(minx, maxx, miny, maxy, crs);
this.create2DCQL(e, bounds , matchAction, enumType);

// We need to handle anti-meridian, we normalize the polygon and may split into two polygon to cover
// two area due to crossing -180 <> 180 line
Geometry g = GeometryUtils.normalizePolygon(GeometryUtils.createPolygon(minx, maxx, miny, maxy));
this.create2DCQL(e, GeometryUtils.toReferencedEnvelope(g,crs) , matchAction, enumType);

} catch (FactoryException fe) {
throw new RuntimeException("Failed to setup bbox SRS", fe);
Expand All @@ -97,17 +106,30 @@ public BBoxImpl(

protected void create2DCQL(
Expression geometry,
BoundingBox bounds,
List<? extends BoundingBox> bounds,
MultiValuedFilter.MatchAction matchAction,
Class<T> enumType) {

this.matchAction = matchAction;
this.geometry = geometry;
T v = Enum.valueOf(enumType, geometry.toString().toLowerCase());
this.query = v.getBoundingBoxQuery(
TopLeftBottomRightGeoBounds.of(builder -> builder
.topLeft(i -> i.latlon(ll -> ll.lon(bounds.getMinX()).lat(bounds.getMaxY())))
.bottomRight(i -> i.latlon(ll -> ll.lon(bounds.getMaxX()).lat(bounds.getMinY())))));
final T v = Enum.valueOf(enumType, geometry.toString().toLowerCase());

if(bounds.size() > 1) {
// Handle multiple bounds by wrapping query with bool:should[]
this.query = BoolQuery.of(f -> f.should(bounds.stream().map(boundingBox -> v.getBoundingBoxQuery(
TopLeftBottomRightGeoBounds.of(builder -> builder
.topLeft(i -> i.latlon(ll -> ll.lon(boundingBox.getMinX()).lat(boundingBox.getMaxY())))
.bottomRight(i -> i.latlon(ll -> ll.lon(boundingBox.getMaxX()).lat(boundingBox.getMinY())))
)))
.toList()))
._toQuery();
}
else {
this.query = v.getBoundingBoxQuery(
TopLeftBottomRightGeoBounds.of(builder -> builder
.topLeft(i -> i.latlon(ll -> ll.lon(bounds.get(0).getMinX()).lat(bounds.get(0).getMaxY())))
.bottomRight(i -> i.latlon(ll -> ll.lon(bounds.get(0).getMaxX()).lat(bounds.get(0).getMinY())))));
}
}

protected void create3DCQL(Expression geometry, BoundingBox3D bounds, MultiValuedFilter.MatchAction matchAction) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,8 +183,7 @@ protected SearchResult searchCollectionBy(final List<Query> queries,
};

try {
double random = Math.random();
log.info("Start search {} {}", ZonedDateTime.now(), random);
log.info("Start search {} {}", ZonedDateTime.now(), Thread.currentThread().getName());
Iterable<Hit<ObjectNode>> response = pagableSearch(builderSupplier, ObjectNode.class, maxSize);

SearchResult result = new SearchResult();
Expand All @@ -198,7 +197,7 @@ protected SearchResult searchCollectionBy(final List<Query> queries,
lastSortValue = i.sort();
}
}
log.info("End search {} {}", ZonedDateTime.now(), random);
log.info("End search {} {}", ZonedDateTime.now(), Thread.currentThread().getName());
// Return the last sort value if exist
if(lastSortValue != null && !lastSortValue.isEmpty()) {
List<Object> values = new ArrayList<>();
Expand Down Expand Up @@ -318,7 +317,7 @@ public Hit<T> next() {
protected StacCollectionModel formatResult(ObjectNode nodes) {
try {
if(nodes != null) {
String json = nodes.toPrettyString();
String json = nodes.toString();
return mapper.readValue(json, StacCollectionModel.class);
}
else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@
import org.geotools.filter.LiteralExpressionImpl;
import org.geotools.geojson.geom.GeometryJSON;
import org.geotools.geometry.jts.JTSFactoryFinder;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.locationtech.jts.geom.*;
import org.locationtech.jts.io.ParseException;
import org.locationtech.jts.io.WKTReader;
import org.locationtech.spatial4j.context.jts.JtsSpatialContext;
import org.locationtech.spatial4j.shape.jts.JtsGeometry;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.operation.TransformException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -236,4 +238,37 @@ public static Geometry createPolygon(double minx, double maxx, double miny, doub
// Create the polygon (no holes)
return factory.createPolygon(shell, null);
}

public static List<ReferencedEnvelope> toReferencedEnvelope(Geometry geometry, CoordinateReferenceSystem crs) {
List<ReferencedEnvelope> result = new ArrayList<>();
if(geometry instanceof MultiPolygon mp) {
for(int i = 0; i < mp.getNumGeometries(); i++) {
if(mp.getGeometryN(i) instanceof Polygon lr) {
Coordinate[] coordinates = lr.getCoordinates();
result.add(toReferencedEnvelope(coordinates, crs));
}
}
}
else if(geometry instanceof Polygon p) {
result.add(toReferencedEnvelope(p.getCoordinates(), crs));
}
return result;
}

public static ReferencedEnvelope toReferencedEnvelope(Coordinate[] coordinates, CoordinateReferenceSystem crs) {
// Initialize bounds
double minx = Double.POSITIVE_INFINITY;
double maxx = Double.NEGATIVE_INFINITY;
double miny = Double.POSITIVE_INFINITY;
double maxy = Double.NEGATIVE_INFINITY;

// Compute bounds
for (Coordinate coord : coordinates) {
minx = Math.min(minx, coord.x);
maxx = Math.max(maxx, coord.x);
miny = Math.min(miny, coord.y);
maxy = Math.max(maxy, coord.y);
}
return new ReferencedEnvelope(minx, maxx, miny, maxy, crs);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.MultiPolygon;
import org.locationtech.jts.geom.Polygon;
import org.locationtech.jts.io.ParseException;
import org.opengis.filter.Filter;
Expand Down Expand Up @@ -140,7 +141,7 @@ public void verifyBBoxWorks1() throws CQLException, IOException, FactoryExceptio
* @throws ParseException - Will not throw
*/
@Test
public void verifyIntersectionWorks2() throws CQLException, IOException, FactoryException, TransformException, ParseException {
public void verifyIntersectionWorks2() throws CQLException, IOException {

// Parse the json and get the noland section
String json = BaseTestClass.readResourceFile("classpath:databag/0015db7e-e684-7548-e053-08114f8cd4ad.json");
Expand Down Expand Up @@ -177,7 +178,7 @@ public void verifyIntersectionWorks2() throws CQLException, IOException, Factory
* @throws ParseException - Will not throw
*/
@Test
public void verifyBBoxWorks2() throws CQLException, IOException, FactoryException, TransformException, ParseException {
public void verifyBBoxWorks2() throws CQLException, IOException {

// Parse the json and get the noland section
String json = BaseTestClass.readResourceFile("classpath:databag/0015db7e-e684-7548-e053-08114f8cd4ad.json");
Expand All @@ -203,4 +204,46 @@ public void verifyBBoxWorks2() throws CQLException, IOException, FactoryExceptio
Assertions.assertEquals(g.getCentroid().getX(), 168.30090846621448, 0.0000001, "getX()");
Assertions.assertEquals(g.getCentroid().getY(), -33.95984804960966, 0.0000001, "getY()");
}
/**
* Similar test as verifyBBoxWorks2, the BBOX not only cross meridian but the sample json have spatial extents
* near equator and span across the whole world
*
* @throws CQLException - Will not throw
* @throws IOException - Will not throw
* @throws FactoryException - Will not throw
* @throws TransformException - Will not throw
* @throws ParseException - Will not throw
*/
@Test
public void verifyBBoxWorks3() throws CQLException, IOException {

// Parse the json and get the noland section
String json = BaseTestClass.readResourceFile("classpath:databag/c9055fe9-921b-44cd-b4f9-a00a1c93e8ac.json");
StacCollectionModel model = mapper.readValue(json, StacCollectionModel.class);

Filter filter = CompilerUtil.parseFilter(
Language.CQL,
"score>=1.5 AND BBOX(geometry,-209.8851491167079,-45.44715475181477,-149.06483661670887,-5.632766095762394)",
factory);

Optional<Geometry> geo = GeometryUtils.readGeometry(model.getSummaries().getGeometryNoLand());

Assertions.assertTrue(geo.isPresent(), "Parse no land correct");
GeometryVisitor visitor = GeometryVisitor.builder()
.build();

// return value are geo applied the CQL, and in this case only BBOX intersected
Geometry g = (Geometry)filter.accept(visitor, geo.get());

Assertions.assertTrue(g instanceof MultiPolygon);

MultiPolygon mp = (MultiPolygon)g;
Assertions.assertEquals(mp.getNumGeometries(), 2, "Geometries correct");

Assertions.assertEquals(mp.getGeometryN(0).getCentroid().getX(), -159.53241830835444, 0.0000001, "getX() for 0");
Assertions.assertEquals(mp.getGeometryN(0).getCentroid().getY(), -19.5, 0.0000001, "getY() for 0");

Assertions.assertEquals(mp.getGeometryN(1).getCentroid().getX(), 151.62121416760516, 0.0000001, "getX() for 1");
Assertions.assertEquals(mp.getGeometryN(1).getCentroid().getY(), -18.000822620336752, 0.0000001, "getY() for 1");
}
}
Loading

0 comments on commit d8444e8

Please sign in to comment.