diff --git a/pom.xml b/pom.xml index e4515ae4..8f401a87 100644 --- a/pom.xml +++ b/pom.xml @@ -118,7 +118,7 @@ org.testcontainers elasticsearch - 1.20.1 + 1.21.3 pom import diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/configuration/CacheConfig.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/configuration/CacheConfig.java index 250da0c5..4722b352 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/configuration/CacheConfig.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/configuration/CacheConfig.java @@ -8,6 +8,7 @@ import org.ehcache.config.units.MemoryUnit; import org.ehcache.jsr107.EhcacheCachingProvider; import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.prep.PreparedGeometry; import org.springframework.cache.annotation.EnableCaching; import org.springframework.cache.jcache.JCacheCacheManager; import org.springframework.context.annotation.Bean; @@ -33,6 +34,7 @@ public class CacheConfig { public static final String ALL_PARAM_VOCABS = "parameter-vocabs"; public static final String ELASTIC_SEARCH_UUID_ONLY = "elastic-search-uuid-only"; public static final String STRING_TO_GEOMETRY = "string-to-geometry"; + public static final String STRING_TO_PREPARE_GEOMETRY = "string-to-prepared-geometry"; @Bean public CacheNoLandGeometry createCacheNoLandGeometry() { @@ -89,6 +91,12 @@ public JCacheCacheManager cacheManager() throws IOException { ResourcePoolsBuilder.heap(20000) ).withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(Duration.ofMinutes(24))) ) + .withCache(STRING_TO_PREPARE_GEOMETRY, + CacheConfigurationBuilder.newCacheConfigurationBuilder( + Map.class, PreparedGeometry.class, + ResourcePoolsBuilder.heap(20000) + ).withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(Duration.ofMinutes(24))) + ) .withCache(GET_CAPABILITIES_WMS_LAYERS, CacheConfigurationBuilder.newCacheConfigurationBuilder( Object.class, Object.class, diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/mapper/Converter.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/mapper/Converter.java index 7cc89cc7..05ee044c 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/mapper/Converter.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/mapper/Converter.java @@ -2,7 +2,6 @@ import au.org.aodn.ogcapi.features.model.*; import au.org.aodn.ogcapi.server.core.model.CitationModel; -import au.org.aodn.ogcapi.server.core.model.AssetModel; import au.org.aodn.ogcapi.server.core.model.ExtendedCollection; import au.org.aodn.ogcapi.server.core.model.ExtendedLink; import au.org.aodn.ogcapi.server.core.model.StacCollectionModel; @@ -15,6 +14,7 @@ import lombok.Getter; import lombok.Setter; import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.prep.PreparedGeometry; import org.opengis.filter.Filter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -156,11 +156,11 @@ default Collection getCollection(D m, Filter fil Map noLand = m.getSummaries().getGeometryNoLand(); if (noLand != null) { // Geometry from elastic search always store in EPSG4326 - GeometryUtils.readGeometry(noLand) + GeometryUtils.readPreparedGeometry(noLand) .ifPresent(input -> { // filter have values if user CQL contains BBox, hence our centroid point needs to be // the noland geometry intersect with BBox and centroid point will be within the BBox - Geometry g = filter != null ? (Geometry) filter.accept(visitor, input) : input; + Geometry g = filter != null ? ((PreparedGeometry) filter.accept(visitor, input)).getGeometry() : input.getGeometry(); collection.getProperties().put( CollectionProperty.centroid, createCentroid(g) diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/parser/stac/GeometryVisitor.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/parser/stac/GeometryVisitor.java index fd5223fe..f7ed9192 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/parser/stac/GeometryVisitor.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/parser/stac/GeometryVisitor.java @@ -4,6 +4,7 @@ import lombok.extern.slf4j.Slf4j; import org.geotools.filter.visitor.DefaultFilterVisitor; import org.locationtech.jts.geom.*; +import org.locationtech.jts.geom.prep.PreparedGeometry; import org.opengis.filter.spatial.*; @Slf4j @@ -116,14 +117,14 @@ public Object visit(Beyond filter, Object data) { @Override public Object visit(BBOX filter, Object data) { if(filter instanceof BBoxImpl impl) { - if(impl.getGeometry() != null && (data instanceof Polygon || data instanceof GeometryCollection)) { - Geometry input = (Geometry) data; + if(impl.getGeometry() != null && (data instanceof Polygon || data instanceof PreparedGeometry)) { + PreparedGeometry input = (PreparedGeometry) data; // buffer is expensive try { - return impl.getGeometry().intersection(input); + return input.getGeometry().intersection(impl.getGeometry()); } catch(Exception e) { - return impl.getGeometry().intersection(input.buffer(0.0)); + return impl.getGeometry().intersection(input.getGeometry().buffer(0.0)); } //return impl.getGeometry().intersection(input.buffer(0.0)); } diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/util/GeometryUtils.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/util/GeometryUtils.java index 1d629f6e..4251e401 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/util/GeometryUtils.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/util/GeometryUtils.java @@ -9,6 +9,8 @@ import org.geotools.geometry.jts.JTSFactoryFinder; import org.geotools.geometry.jts.ReferencedEnvelope; import org.locationtech.jts.geom.*; +import org.locationtech.jts.geom.prep.PreparedGeometry; +import org.locationtech.jts.geom.prep.PreparedGeometryFactory; import org.locationtech.jts.io.ParseException; import org.locationtech.jts.io.WKTReader; import org.locationtech.jts.io.WKTWriter; @@ -29,6 +31,7 @@ import java.util.concurrent.*; import static au.org.aodn.ogcapi.server.core.configuration.CacheConfig.STRING_TO_GEOMETRY; +import static au.org.aodn.ogcapi.server.core.configuration.CacheConfig.STRING_TO_PREPARE_GEOMETRY; public class GeometryUtils { @@ -211,7 +214,26 @@ public static String convertToGeoJson(LiteralExpressionImpl literalExpression, C return r; } } - + /** + * Many code require static access to this function, hence we will init it somewhere as bean and then + * set it to the static instance for sharing + * + * @param input - A geometry text input + * @return - The converted geometry + */ + public static Optional readPreparedGeometry(Object input) { + return self.readCachedPreparedGeometry(input); + } + /** + * Please use this function as it contains the parser with enough decimal to make it work. + * + * @param input - A Json of GeoJson + * @return - A prepared geometry for fast intersect calculation + */ + @Cacheable(STRING_TO_PREPARE_GEOMETRY) + public Optional readCachedPreparedGeometry(Object input) { + return self.readCachedGeometry(input).map(PreparedGeometryFactory::prepare); + } /** * Many code require static access to this function, hence we will init it somewhere as bean and then * set it to the static instance for sharing @@ -222,7 +244,6 @@ public static String convertToGeoJson(LiteralExpressionImpl literalExpression, C public static Optional readGeometry(Object input) { return self.readCachedGeometry(input); } - /** * Please use this function as it contains the parser with enough decimal to make it work. * @@ -244,7 +265,6 @@ public Optional readCachedGeometry(Object input) { } return Optional.empty(); } - /** * Normalize a polygon by adjusting longitudes to the range [-180, 180], and return both parts as a GeometryCollection. * diff --git a/server/src/test/java/au/org/aodn/ogcapi/server/core/parser/stac/ParserTest.java b/server/src/test/java/au/org/aodn/ogcapi/server/core/parser/stac/ParserTest.java index 2a8fd6c0..4b6af837 100644 --- a/server/src/test/java/au/org/aodn/ogcapi/server/core/parser/stac/ParserTest.java +++ b/server/src/test/java/au/org/aodn/ogcapi/server/core/parser/stac/ParserTest.java @@ -17,8 +17,8 @@ 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.geom.GeometryCollection; +import org.locationtech.jts.geom.prep.PreparedGeometry; import org.locationtech.jts.io.ParseException; import org.opengis.filter.Filter; import org.opengis.referencing.FactoryException; @@ -74,7 +74,7 @@ public void verifyBBoxWorks1() throws CQLException, IOException, FactoryExceptio StacCollectionModel model = mapper.readValue(json, StacCollectionModel.class); Filter filter = CompilerUtil.parseFilter(Language.CQL, param.getFilter(), factory); - Optional geo = GeometryUtils.readGeometry(model.getSummaries().getGeometryNoLand()); + Optional geo = GeometryUtils.readPreparedGeometry(model.getSummaries().getGeometryNoLand()); Assertions.assertTrue(geo.isPresent(), "Parse no land correct"); GeometryVisitor visitor = GeometryVisitor.builder() @@ -83,7 +83,7 @@ public void verifyBBoxWorks1() throws CQLException, IOException, FactoryExceptio // return value are geo applied the CQL, and in this case only INTERSECTS Geometry g = (Geometry)filter.accept(visitor, geo.get()); Assertions.assertTrue(expected.isPresent(), "Expected parse correct"); - Assertions.assertEquals(g, expected.get(), "They are equals"); + Assertions.assertEquals(g.getGeometryN(0), expected.get(), "They are equals"); } /** * Test almost the same as the verifyIntersectionWorks2, since verifyIntersectionWorks1 create a polygon same as box @@ -104,7 +104,7 @@ public void verifyBBoxWorks2() throws CQLException, IOException { "score>=1.5 AND BBOX(geometry,-203.16603491348164,-60.248194404495756,-86.85117538227594,15.902738674628525)", factory); - Optional geo = GeometryUtils.readGeometry(model.getSummaries().getGeometryNoLand()); + Optional geo = GeometryUtils.readPreparedGeometry(model.getSummaries().getGeometryNoLand()); Assertions.assertTrue(geo.isPresent(), "Parse no land correct"); GeometryVisitor visitor = GeometryVisitor.builder() @@ -114,7 +114,7 @@ public void verifyBBoxWorks2() throws CQLException, IOException { Geometry g = (Geometry)filter.accept(visitor, geo.get()); Assertions.assertFalse(g.isEmpty()); - Assertions.assertInstanceOf(Polygon.class, g); + Assertions.assertInstanceOf(GeometryCollection.class, g); Assertions.assertEquals(168.30090846621448, g.getCentroid().getX(), 0.0000001, "getX()"); Assertions.assertEquals(-33.95984804960966, g.getCentroid().getY(), 0.0000001, "getY()"); @@ -138,7 +138,7 @@ public void verifyBBoxWorks3() throws CQLException, IOException { "score>=1.5 AND BBOX(geometry,-209.8851491167079,-45.44715475181477,-149.06483661670887,-5.632766095762394)", factory); - Optional geo = GeometryUtils.readGeometry(model.getSummaries().getGeometryNoLand()); + Optional geo = GeometryUtils.readPreparedGeometry(model.getSummaries().getGeometryNoLand()); Assertions.assertTrue(geo.isPresent(), "Parse no land correct"); GeometryVisitor visitor = GeometryVisitor.builder() @@ -147,15 +147,15 @@ public void verifyBBoxWorks3() throws CQLException, IOException { // return value are geo applied the CQL, and in this case only BBOX intersected Geometry g = (Geometry)filter.accept(visitor, geo.get()); - Assertions.assertInstanceOf(MultiPolygon.class, g); + Assertions.assertInstanceOf(GeometryCollection.class, g); - MultiPolygon mp = (MultiPolygon)g; + GeometryCollection mp = (GeometryCollection)g; Assertions.assertEquals(2, mp.getNumGeometries(), "Geometries correct"); - Assertions.assertEquals(-159.53241830835444, mp.getGeometryN(1).getCentroid().getX(), 0.0000001, "getX() for 0"); - Assertions.assertEquals(-19.5, mp.getGeometryN(1).getCentroid().getY(), 0.0000001, "getY() for 0"); + Assertions.assertEquals(-159.53241830835444, mp.getGeometryN(0).getCentroid().getX(), 0.0000001, "getX() for 0"); + Assertions.assertEquals(-19.5, mp.getGeometryN(0).getCentroid().getY(), 0.0000001, "getY() for 0"); - Assertions.assertEquals(151.62121416760516, mp.getGeometryN(0).getCentroid().getX(), 0.0000001, "getX() for 1"); - Assertions.assertEquals(-18.000822620336752, mp.getGeometryN(0).getCentroid().getY(), 0.0000001, "getY() for 1"); + Assertions.assertEquals(151.62121416760516, mp.getGeometryN(1).getCentroid().getX(), 0.0000001, "getX() for 1"); + Assertions.assertEquals(-18.000822620336752, mp.getGeometryN(1).getCentroid().getY(), 0.0000001, "getY() for 1"); } }