Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bug fix on BBOX cross meridian #97

Merged
merged 1 commit into from
Nov 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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