diff --git a/src/hakunapi-core/src/main/java/fi/nls/hakunapi/core/FilterParser.java b/src/hakunapi-core/src/main/java/fi/nls/hakunapi/core/FilterParser.java index 9490f65e..1fb95c7d 100644 --- a/src/hakunapi-core/src/main/java/fi/nls/hakunapi/core/FilterParser.java +++ b/src/hakunapi-core/src/main/java/fi/nls/hakunapi/core/FilterParser.java @@ -5,6 +5,6 @@ public interface FilterParser { public String getCode(); - public Filter parse(FeatureType ft, String filter, int filterSrid) throws IllegalArgumentException; + public Filter parse(FeatureType ft, String filter, SRIDCode filterSrid) throws IllegalArgumentException; } diff --git a/src/hakunapi-core/src/main/java/fi/nls/hakunapi/core/SRIDCode.java b/src/hakunapi-core/src/main/java/fi/nls/hakunapi/core/SRIDCode.java index 745c4aef..2a1c1b43 100644 --- a/src/hakunapi-core/src/main/java/fi/nls/hakunapi/core/SRIDCode.java +++ b/src/hakunapi-core/src/main/java/fi/nls/hakunapi/core/SRIDCode.java @@ -5,8 +5,12 @@ public class SRIDCode { - public static final SRIDCode CRS84 = new SRIDCode(Crs.CRS84_SRID, false, true, HakunaGeometryDimension.XY); - public static final SRIDCode WGS84 = new SRIDCode(4326, true, true, HakunaGeometryDimension.XY); + // Geographic wrap bounds (degrees) + private static final double GEO_WRAP_X_MIN = -180.0; + private static final double GEO_WRAP_X_MAX = 180.0; + + public static final SRIDCode CRS84 = new SRIDCode(Crs.CRS84_SRID, false, true, HakunaGeometryDimension.XY, GEO_WRAP_X_MIN, GEO_WRAP_X_MAX); + public static final SRIDCode WGS84 = new SRIDCode(4326, true, true, HakunaGeometryDimension.XY, GEO_WRAP_X_MIN, GEO_WRAP_X_MAX); public static boolean isKnown(int srid) { return srid == CRS84.srid || srid == WGS84.srid; @@ -20,12 +24,21 @@ public static SRIDCode getKnown(int srid) { private final boolean latLon; private final boolean degrees; private final HakunaGeometryDimension dimension; + private final Double wrapXMin; + private final Double wrapXMax; public SRIDCode(int srid, boolean latLon, boolean degrees, HakunaGeometryDimension dimension) { + this(srid, latLon, degrees, dimension, null, null); + } + + public SRIDCode(int srid, boolean latLon, boolean degrees, HakunaGeometryDimension dimension, + Double wrapXMin, Double wrapXMax) { this.srid = srid; this.latLon = latLon; this.degrees = degrees; this.dimension = dimension; + this.wrapXMin = wrapXMin; + this.wrapXMax = wrapXMax; } public int getSrid() { @@ -44,8 +57,20 @@ public HakunaGeometryDimension getDimension() { return dimension; } + public Double getWrapXMin() { + return wrapXMin; + } + + public Double getWrapXMax() { + return wrapXMax; + } + + public boolean supportsWrapX() { + return wrapXMin != null && wrapXMax != null; + } + public SRIDCode withDimension(HakunaGeometryDimension d) { - return new SRIDCode(this.srid, this.latLon, this.degrees, d); + return new SRIDCode(this.srid, this.latLon, this.degrees, d, this.wrapXMin, this.wrapXMax); } } diff --git a/src/hakunapi-core/src/main/java/fi/nls/hakunapi/core/geom/Bbox.java b/src/hakunapi-core/src/main/java/fi/nls/hakunapi/core/geom/Bbox.java index 37fe8895..b52c0cf2 100644 --- a/src/hakunapi-core/src/main/java/fi/nls/hakunapi/core/geom/Bbox.java +++ b/src/hakunapi-core/src/main/java/fi/nls/hakunapi/core/geom/Bbox.java @@ -4,11 +4,50 @@ import org.locationtech.jts.geom.Envelope; import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.MultiPolygon; +import org.locationtech.jts.geom.Polygon; public class Bbox { private Bbox() {} + public static Coordinate[] parseCoordinates(String bbox) throws IllegalArgumentException { + if (bbox == null || bbox.length() == 0) { + return null; + } + + String[] split = bbox.split(","); + if (split.length != 4 && split.length != 6) { + throw new IllegalArgumentException("Invalid number of elements in bbox!"); + } + + double[] numbers = new double[split.length]; + try { + for (int i = 0; i < numbers.length; i++) { + numbers[i] = Double.parseDouble(split[i]); + } + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid bbox!"); + } + + if (numbers.length == 4) { + return new Coordinate[] { + new Coordinate(numbers[0], numbers[1]), + new Coordinate(numbers[2], numbers[3]) + }; + } else { + return new Coordinate[] { + new Coordinate(numbers[0], numbers[1], numbers[2]), + new Coordinate(numbers[3], numbers[4], numbers[5]) + }; + } + } + + /** + * @deprecated Envelope does not handle antimeridian-spanning bboxes correctly. + * Use parseCoordinates() instead. + */ + @Deprecated public static Envelope parse(String bbox) throws IllegalArgumentException { if (bbox == null || bbox.length() == 0) { return null; @@ -28,10 +67,20 @@ public static Envelope parse(String bbox) throws IllegalArgumentException { } } + /** + * @deprecated Envelope does not handle antimeridian-spanning bboxes correctly. + * Use parseCoordinates() instead. + */ + @Deprecated public static Geometry toGeometry(Envelope env) { return toGeometry(env, HakunaGeometryFactory.GF); } + /** + * @deprecated Envelope does not handle antimeridian-spanning bboxes correctly. + * Use parseCoordinates() instead. + */ + @Deprecated public static Geometry toGeometry(Envelope env, GeometryFactory gf) { if (gf == null) { gf = HakunaGeometryFactory.GF; @@ -46,4 +95,156 @@ public static Geometry toGeometry(Envelope env, GeometryFactory gf) { return gf.createPolygon(shell); } + /** + * Creates a bbox geometry. When x1 > x2, creates a polygon with "inverted" x coordinates + * which can be detected and split later based on SRID wrap bounds. + * + * @param x1 first x coordinate (minX for normal, west edge for wrap-x crossing) + * @param y1 first y coordinate (minY) + * @param x2 second x coordinate (maxX for normal, east edge for wrap-x crossing) + * @param y2 second y coordinate (maxY) + * @param gf GeometryFactory to use (null for default) + * @return Polygon geometry representing the bbox (may need wrap-x splitting later) + */ + public static Geometry createBboxGeometry(double x1, double y1, double x2, double y2, + GeometryFactory gf) { + if (gf == null) { + gf = HakunaGeometryFactory.GF; + } + // Create polygon with original coordinates - don't normalize x1/x2 + // This preserves the "inverted" x order for antimeridian detection later + Coordinate[] shell = { + new Coordinate(x1, y1), + new Coordinate(x2, y1), + new Coordinate(x2, y2), + new Coordinate(x1, y2), + new Coordinate(x1, y1) + }; + return gf.createPolygon(shell); + } + + /** + * Checks if a geometry is a bbox-like rectangle that crosses the wrap-x boundary. + * Detects this by finding exactly two unique x values and checking if the "left" one + * (appearing first in the coordinate sequence) is greater than the "right" one. + * + * @param geom the geometry to check + * @return true if the geometry appears to be an antimeridian-crossing bbox + */ + public static boolean isWrapXCrossingBbox(Geometry geom) { + if (!(geom instanceof Polygon)) { + return false; + } + Coordinate[] coords = geom.getCoordinates(); + if (coords.length != 5) { + return false; // Not a simple rectangle + } + + // Find the two unique x values + double firstX = coords[0].x; + double secondX = Double.NaN; + for (int i = 1; i < coords.length - 1; i++) { + if (coords[i].x != firstX) { + secondX = coords[i].x; + break; + } + } + + if (Double.isNaN(secondX)) { + return false; // Degenerate case + } + + // If firstX > secondX, the bbox crosses the wrap boundary + return firstX > secondX; + } + + /** + * Splits a wrap-x crossing bbox into a MultiPolygon. + * Should only be called after isWrapXCrossingBbox returns true. + * + * @param geom the bbox polygon to split + * @param wrapXMin minimum x boundary (e.g., -180) + * @param wrapXMax maximum x boundary (e.g., 180) + * @return MultiPolygon with western and eastern parts + */ + public static Geometry splitWrapXBbox(Geometry geom, double wrapXMin, double wrapXMax) { + Coordinate[] coords = geom.getCoordinates(); + + // Find the two unique x values and two unique y values + double x1 = coords[0].x; + double x2 = Double.NaN; + double y1 = coords[0].y; + double y2 = Double.NaN; + + for (int i = 1; i < coords.length - 1; i++) { + if (Double.isNaN(x2) && coords[i].x != x1) { + x2 = coords[i].x; + } + if (Double.isNaN(y2) && coords[i].y != y1) { + y2 = coords[i].y; + } + } + + // Ensure y1 < y2 + if (y1 > y2) { + double tmp = y1; + y1 = y2; + y2 = tmp; + } + + GeometryFactory gf = geom.getFactory(); + Geometry result = createWrapXMultiPolygon(x1, y1, x2, y2, wrapXMin, wrapXMax, gf); + result.setSRID(geom.getSRID()); + return result; + } + + private static Geometry createWrapXMultiPolygon( + double x1, double y1, double x2, double y2, + double wrapXMin, double wrapXMax, + GeometryFactory gf) { + // Western box: from x1 to wrap boundary maximum + Coordinate[] westernShell = { + new Coordinate(x1, y1), + new Coordinate(wrapXMax, y1), + new Coordinate(wrapXMax, y2), + new Coordinate(x1, y2), + new Coordinate(x1, y1) + }; + Polygon westernBox = gf.createPolygon(westernShell); + + // Eastern box: from wrap boundary minimum to x2 + Coordinate[] easternShell = { + new Coordinate(wrapXMin, y1), + new Coordinate(x2, y1), + new Coordinate(x2, y2), + new Coordinate(wrapXMin, y2), + new Coordinate(wrapXMin, y1) + }; + Polygon easternBox = gf.createPolygon(easternShell); + + return gf.createMultiPolygon(new Polygon[]{westernBox, easternBox}); + } + + + /** + * If the MultiPolygon has exactly 2 touching polygons, merge them into one. + * This can happen after reprojection when the storage CRS doesn't have a wrap boundary + * (e.g., reprojecting from CRS84 to a local CRS like EPSG:3067). + * + * @param mp the MultiPolygon to potentially merge + * @return merged Polygon if the two parts touch, otherwise the original MultiPolygon + */ + public static Geometry tryMergeMultiPolygon(MultiPolygon mp) { + if (mp.getNumGeometries() == 2) { + Polygon p1 = (Polygon) mp.getGeometryN(0); + Polygon p2 = (Polygon) mp.getGeometryN(1); + if (p1.touches(p2)) { + Geometry merged = p1.union(p2); + merged.setSRID(mp.getSRID()); + return merged; + } + } + return mp; + } + } diff --git a/src/hakunapi-core/src/main/java/fi/nls/hakunapi/core/param/BboxCrsParam.java b/src/hakunapi-core/src/main/java/fi/nls/hakunapi/core/param/BboxCrsParam.java index aff6937d..64326dac 100644 --- a/src/hakunapi-core/src/main/java/fi/nls/hakunapi/core/param/BboxCrsParam.java +++ b/src/hakunapi-core/src/main/java/fi/nls/hakunapi/core/param/BboxCrsParam.java @@ -1,15 +1,10 @@ package fi.nls.hakunapi.core.param; -import org.locationtech.jts.geom.Geometry; - import fi.nls.hakunapi.core.FeatureServiceConfig; -import fi.nls.hakunapi.core.filter.Filter; import fi.nls.hakunapi.core.property.simple.HakunaPropertyGeometry; import fi.nls.hakunapi.core.request.GetFeatureCollection; import fi.nls.hakunapi.core.request.GetFeatureRequest; -import fi.nls.hakunapi.core.util.AxisOrderSwapFilter; import fi.nls.hakunapi.core.util.CrsUtil; -import fi.nls.hakunapi.core.util.FilterUtil; import io.swagger.v3.oas.models.media.StringSchema; import io.swagger.v3.oas.models.parameters.Parameter; import io.swagger.v3.oas.models.parameters.Parameter.StyleEnum; @@ -39,33 +34,22 @@ public void modify(FeatureServiceConfig service, GetFeatureRequest request, Stri return; } + int srid = CrsUtil.parseSRID(value, getParamName()); + for (GetFeatureCollection collection : request.getCollections()) { HakunaPropertyGeometry geom = collection.getFt().getGeom(); - if (geom == null) { - continue; - } - Filter bboxFilter = FilterUtil.findFilterByTag(collection.getFilters(), BboxParam.TAG); - if (bboxFilter == null) { - continue; - } - int srid = CrsUtil.parseSRID(value, getParamName()); - if (!geom.isSRIDSupported(srid)) { + if (geom != null && !geom.isSRIDSupported(srid)) { throw new IllegalArgumentException(CrsUtil.ERR_UNSUPPORTED_CRS); } - Geometry bbox = (Geometry) bboxFilter.getValue(); - bbox.setSRID(srid); - - if (service.isCrsLatLon(srid)) { - bbox.apply(new AxisOrderSwapFilter()); - } } - + + request.setBboxSrid(srid); request.addQueryParam(getParamName(), value); } @Override public int priority() { - return 10; // Run after BboxParam + return -10; // Run before BboxParam } } diff --git a/src/hakunapi-core/src/main/java/fi/nls/hakunapi/core/param/BboxParam.java b/src/hakunapi-core/src/main/java/fi/nls/hakunapi/core/param/BboxParam.java index 8d05fa8b..702c776e 100644 --- a/src/hakunapi-core/src/main/java/fi/nls/hakunapi/core/param/BboxParam.java +++ b/src/hakunapi-core/src/main/java/fi/nls/hakunapi/core/param/BboxParam.java @@ -1,15 +1,17 @@ package fi.nls.hakunapi.core.param; -import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.MultiPolygon; import fi.nls.hakunapi.core.FeatureServiceConfig; +import fi.nls.hakunapi.core.SRIDCode; import fi.nls.hakunapi.core.filter.Filter; import fi.nls.hakunapi.core.geom.Bbox; +import fi.nls.hakunapi.core.projection.ProjectionHelper; import fi.nls.hakunapi.core.property.simple.HakunaPropertyGeometry; import fi.nls.hakunapi.core.request.GetFeatureCollection; import fi.nls.hakunapi.core.request.GetFeatureRequest; -import fi.nls.hakunapi.core.schemas.Crs; import io.swagger.v3.oas.models.media.ArraySchema; import io.swagger.v3.oas.models.media.NumberSchema; import io.swagger.v3.oas.models.parameters.Parameter; @@ -64,18 +66,65 @@ public void modify(FeatureServiceConfig service, GetFeatureRequest request, Stri return; } - Envelope env = Bbox.parse(value); - Geometry bbox = Bbox.toGeometry(env); - bbox.setSRID(Crs.CRS84_SRID); + int bboxSrid = request.getBboxSrid(); + + Coordinate[] coordinates = Bbox.parseCoordinates(value); + + // Get SRIDCode for wrap-x bounds + SRIDCode sridCode = service.getSridCode(bboxSrid) + .orElseThrow(() -> new IllegalArgumentException("Unknown SRID: " + bboxSrid)); + + // Handle lat/lon axis order (swap before checking wrap-x) + if (sridCode.isLatLon()) { + for (Coordinate c : coordinates) { + swapAxisOrder(c); + } + } + + double x1 = coordinates[0].x; + double y1 = coordinates[0].y; + double x2 = coordinates[1].x; + double y2 = coordinates[1].y; + + // Create bbox geometry (preserves coordinate order for wrap-x detection) + Geometry bbox = Bbox.createBboxGeometry(x1, y1, x2, y2, null); + bbox.setSRID(bboxSrid); + + // Check if this is a bbox that crosses the wrap-x boundary (antimeridian) + if (Bbox.isWrapXCrossingBbox(bbox)) { + if (sridCode.supportsWrapX()) { + bbox = Bbox.splitWrapXBbox(bbox, sridCode.getWrapXMin(), sridCode.getWrapXMax()); + } else { + throw new IllegalArgumentException( + "Invalid bbox: west-most edge is larger than east-most edge. " + + "This is only valid for coordinate systems that support wrap-x."); + } + } + + boolean wasMultiPolygon = bbox instanceof MultiPolygon; for (GetFeatureCollection c : request.getCollections()) { HakunaPropertyGeometry geometryProp = c.getFt().getGeom(); if (geometryProp != null) { - c.addFilter(Filter.intersects(geometryProp, bbox).setTag(TAG)); + Geometry geom = ProjectionHelper.reprojectToStorageCRS(geometryProp, bbox); + + // Optimization: if reprojection caused the two polygons to touch, merge them + if (wasMultiPolygon && geom instanceof MultiPolygon) { + geom = Bbox.tryMergeMultiPolygon((MultiPolygon) geom); + } + + c.addFilter(Filter.intersects(geometryProp, geom).setTag(TAG)); } } request.addQueryParam(getParamName(), value); } + private static void swapAxisOrder(Coordinate c) { + final double x = c.getX(); + final double y = c.getY(); + c.setX(y); + c.setY(x); + } + } diff --git a/src/hakunapi-core/src/main/java/fi/nls/hakunapi/core/param/FilterParam.java b/src/hakunapi-core/src/main/java/fi/nls/hakunapi/core/param/FilterParam.java index 701ac3d2..8afe28e5 100644 --- a/src/hakunapi-core/src/main/java/fi/nls/hakunapi/core/param/FilterParam.java +++ b/src/hakunapi-core/src/main/java/fi/nls/hakunapi/core/param/FilterParam.java @@ -2,6 +2,7 @@ import fi.nls.hakunapi.core.FeatureType; import fi.nls.hakunapi.core.FilterParser; +import fi.nls.hakunapi.core.SRIDCode; import fi.nls.hakunapi.core.FeatureServiceConfig; import fi.nls.hakunapi.core.filter.Filter; import fi.nls.hakunapi.core.request.GetFeatureCollection; @@ -53,9 +54,12 @@ public void modify(FeatureServiceConfig service, GetFeatureRequest request, Stri throw new IllegalArgumentException(err); } + SRIDCode filterSrid = service.getSridCode(request.getFilterSrid()) + .orElseThrow(() -> new IllegalArgumentException("Unknown SRID: " + request.getFilterSrid())); + for (GetFeatureCollection c : request.getCollections()) { FeatureType ft = c.getFt(); - Filter filter = parser.parse(ft, value, request.getFilterSrid()); + Filter filter = parser.parse(ft, value, filterSrid); if (filter != null) { filter.setTag(TAG); c.addFilter(filter); diff --git a/src/hakunapi-core/src/main/java/fi/nls/hakunapi/core/request/GetFeatureRequest.java b/src/hakunapi-core/src/main/java/fi/nls/hakunapi/core/request/GetFeatureRequest.java index a2fee362..1df51eab 100644 --- a/src/hakunapi-core/src/main/java/fi/nls/hakunapi/core/request/GetFeatureRequest.java +++ b/src/hakunapi-core/src/main/java/fi/nls/hakunapi/core/request/GetFeatureRequest.java @@ -17,6 +17,7 @@ public class GetFeatureRequest { private int offset; private int limit; private int srid = Crs.CRS84_SRID; + private int bboxSrid = Crs.CRS84_SRID; private int filterSrid = Crs.CRS84_SRID; private Map pathParams; private Map queryParams; @@ -74,6 +75,14 @@ public void setSRID(int srid) { this.srid = srid; } + public int getBboxSrid() { + return bboxSrid; + } + + public void setBboxSrid(int srid) { + this.bboxSrid = srid; + } + public int getFilterSrid() { return filterSrid; } diff --git a/src/hakunapi-core/src/main/java/fi/nls/hakunapi/simple/servlet/operation/param/WKTGeometryOperationParam.java b/src/hakunapi-core/src/main/java/fi/nls/hakunapi/simple/servlet/operation/param/WKTGeometryOperationParam.java index 28f0549f..94595eee 100644 --- a/src/hakunapi-core/src/main/java/fi/nls/hakunapi/simple/servlet/operation/param/WKTGeometryOperationParam.java +++ b/src/hakunapi-core/src/main/java/fi/nls/hakunapi/simple/servlet/operation/param/WKTGeometryOperationParam.java @@ -9,6 +9,7 @@ import fi.nls.hakunapi.core.filter.Filter; import fi.nls.hakunapi.core.geom.EWKTReader; import fi.nls.hakunapi.core.geom.HakunaGeometryFactory; +import fi.nls.hakunapi.core.projection.ProjectionHelper; import fi.nls.hakunapi.core.property.HakunaProperty; import fi.nls.hakunapi.core.property.simple.HakunaPropertyGeometry; import fi.nls.hakunapi.core.request.GetFeatureRequest; @@ -93,6 +94,7 @@ private Filter toFilter(String value) { } private Filter toFilter(Geometry g) { + g = ProjectionHelper.reprojectToStorageCRS(geom, g); switch (op) { case INTERSECTS: return Filter.intersects(geom, g); diff --git a/src/hakunapi-core/src/test/java/fi/nls/hakunapi/core/param/BboxAntimeridianTest.java b/src/hakunapi-core/src/test/java/fi/nls/hakunapi/core/param/BboxAntimeridianTest.java new file mode 100644 index 00000000..a35dfce4 --- /dev/null +++ b/src/hakunapi-core/src/test/java/fi/nls/hakunapi/core/param/BboxAntimeridianTest.java @@ -0,0 +1,327 @@ +package fi.nls.hakunapi.core.param; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.MultiPolygon; +import org.locationtech.jts.geom.Point; +import org.locationtech.jts.geom.Polygon; + +import fi.nls.hakunapi.core.FeatureProducer; +import fi.nls.hakunapi.core.FeatureServiceConfig; +import fi.nls.hakunapi.core.FeatureType; +import fi.nls.hakunapi.core.OutputFormat; +import fi.nls.hakunapi.core.PaginationStrategyOffset; +import fi.nls.hakunapi.core.SRIDCode; +import fi.nls.hakunapi.core.SimpleFeatureType; +import fi.nls.hakunapi.core.filter.Filter; +import fi.nls.hakunapi.core.geom.HakunaGeometryFactory; +import fi.nls.hakunapi.core.projection.NOPProjectionTransformer; +import fi.nls.hakunapi.core.projection.ProjectionTransformer; +import fi.nls.hakunapi.core.projection.ProjectionTransformerFactory; +import fi.nls.hakunapi.core.property.simple.HakunaPropertyGeometry; +import fi.nls.hakunapi.core.request.GetFeatureCollection; +import fi.nls.hakunapi.core.request.GetFeatureRequest; +import fi.nls.hakunapi.core.schemas.Crs; + +/** + * Tests for antimeridian-spanning bbox queries (Issue #79) + * + * These tests verify that bbox queries correctly handle the antimeridian + * (180°/-180° longitude line). When a bbox has minX > maxX, it indicates + * the bbox spans the antimeridian and should be split into two regions: + * - Western region: minX to 180° + * - Eastern region: -180° to maxX + */ +public class BboxAntimeridianTest { + + private SimpleFeatureType collection; + private FeatureServiceConfig service; + + @Before + public void init() { + collection = new SimpleFeatureType() { + @Override + public FeatureProducer getFeatureProducer() { + return null; + } + }; + collection.setName("antimeridian_test"); + collection.setProperties(Collections.emptyList()); + collection.setStaticFilters(Collections.emptyList()); + collection.setGeom(new HakunaPropertyGeometry("geom", "table", null, false, null, new int[0], 84, 2, (__, ___, ____) -> {})); + collection.setPaginationStrategy(PaginationStrategyOffset.INSTANCE); + collection.setProjectionTransformerFactory(new ProjectionTransformerFactory() { + @Override + public ProjectionTransformer getTransformer(int fromSRID, int toSRID) { + return NOPProjectionTransformer.INSTANCE; + } + @Override + public ProjectionTransformer toCRS84(int fromSRID) { + return NOPProjectionTransformer.INSTANCE; + } + @Override + public ProjectionTransformer fromCRS84(int toSRID) { + return NOPProjectionTransformer.INSTANCE; + } + }); + + service = new FeatureServiceConfig() { + @Override + public Collection getOutputFormats() { + return null; + } + + @Override + public OutputFormat getOutputFormat(String f) { + return null; + } + + @Override + public Collection getCollections() { + return Arrays.asList(collection); + } + + @Override + public FeatureType getCollection(String name) { + return collection.getName().equals(name) ? collection : null; + } + }; + + service.setKnownSrids(Arrays.asList(SRIDCode.CRS84, SRIDCode.WGS84)); + } + + /** + * Test Issue #79 bbox: 160.6,-55.95,-170,-25.89 + * This bbox spans the antimeridian and should create a MultiPolygon + */ + @Test + public void testIssue79AntimeridianBbox() { + GetFeatureRequest req = new GetFeatureRequest(); + GetFeatureCollection getItems = new GetFeatureCollection(collection); + req.addCollection(getItems); + + new BboxParam().modify(service, req, "160.6,-55.95,-170,-25.89"); + new BboxCrsParam().modify(service, req, Crs.CRS84); + + List filters = getItems.getFilters(); + assertEquals(1, filters.size()); + + Filter filter = filters.get(0); + Geometry geom = (Geometry) filter.getValue(); + + assertEquals(84, geom.getSRID()); + assertEquals("MultiPolygon", geom.getGeometryType()); + assertTrue(geom instanceof MultiPolygon); + + MultiPolygon mp = (MultiPolygon) geom; + assertEquals(2, mp.getNumGeometries()); + + Polygon western = (Polygon) mp.getGeometryN(0); + Polygon eastern = (Polygon) mp.getGeometryN(1); + + // Western box: 160.6 to 180 + Coordinate[] westernCoords = western.getCoordinates(); + assertEquals(160.6, westernCoords[0].getX(), 0.001); + assertEquals(180.0, westernCoords[1].getX(), 0.001); + + // Eastern box: -180 to -170 + Coordinate[] easternCoords = eastern.getCoordinates(); + assertEquals(-180.0, easternCoords[0].getX(), 0.001); + assertEquals(-170.0, easternCoords[1].getX(), 0.001); + } + + /** + * Test that points west of antimeridian (160.6° to 180°) intersect western box + */ + @Test + public void testPointsInWesternPartOfAntimeridianBbox() { + GetFeatureRequest req = new GetFeatureRequest(); + GetFeatureCollection getItems = new GetFeatureCollection(collection); + req.addCollection(getItems); + + new BboxParam().modify(service, req, "160.6,-55.95,-170,-25.89"); + + Filter filter = getItems.getFilters().get(0); + Geometry bboxGeom = (Geometry) filter.getValue(); + + // Points that SHOULD be in western part + Point westPoint1 = HakunaGeometryFactory.GF.createPoint(new Coordinate(165.0, -30.0)); + Point westPoint2 = HakunaGeometryFactory.GF.createPoint(new Coordinate(170.0, -35.0)); + Point westPoint3 = HakunaGeometryFactory.GF.createPoint(new Coordinate(175.0, -40.0)); + + assertTrue("Point at 165°, -30° should be in western part", bboxGeom.intersects(westPoint1)); + assertTrue("Point at 170°, -35° should be in western part", bboxGeom.intersects(westPoint2)); + assertTrue("Point at 175°, -40° should be in western part", bboxGeom.intersects(westPoint3)); + } + + /** + * Test that points east of antimeridian (-180° to -170°) intersect eastern box + */ + @Test + public void testPointsInEasternPartOfAntimeridianBbox() { + GetFeatureRequest req = new GetFeatureRequest(); + GetFeatureCollection getItems = new GetFeatureCollection(collection); + req.addCollection(getItems); + + new BboxParam().modify(service, req, "160.6,-55.95,-170,-25.89"); + + Filter filter = getItems.getFilters().get(0); + Geometry bboxGeom = (Geometry) filter.getValue(); + + // Points that SHOULD be in eastern part + Point eastPoint1 = HakunaGeometryFactory.GF.createPoint(new Coordinate(-175.0, -30.0)); + Point eastPoint2 = HakunaGeometryFactory.GF.createPoint(new Coordinate(-172.0, -35.0)); + Point eastPoint3 = HakunaGeometryFactory.GF.createPoint(new Coordinate(-170.5, -40.0)); + + assertTrue("Point at -175°, -30° should be in eastern part", bboxGeom.intersects(eastPoint1)); + assertTrue("Point at -172°, -35° should be in eastern part", bboxGeom.intersects(eastPoint2)); + assertTrue("Point at -170.5°, -40° should be in eastern part", bboxGeom.intersects(eastPoint3)); + } + + /** + * Test that points outside the bbox do NOT intersect + */ + @Test + public void testPointsOutsideAntimeridianBbox() { + GetFeatureRequest req = new GetFeatureRequest(); + GetFeatureCollection getItems = new GetFeatureCollection(collection); + req.addCollection(getItems); + + new BboxParam().modify(service, req, "160.6,-55.95,-170,-25.89"); + + Filter filter = getItems.getFilters().get(0); + Geometry bboxGeom = (Geometry) filter.getValue(); + + // Points that should NOT be in bbox + Point outsideWest = HakunaGeometryFactory.GF.createPoint(new Coordinate(150.0, -30.0)); // Too far west + Point outsideEast = HakunaGeometryFactory.GF.createPoint(new Coordinate(-160.0, -30.0)); // Too far east + Point outsideNorth = HakunaGeometryFactory.GF.createPoint(new Coordinate(170.0, -20.0)); // Too far north + Point outsideSouth = HakunaGeometryFactory.GF.createPoint(new Coordinate(170.0, -60.0)); // Too far south + + assertTrue("Point at 150° should be OUTSIDE (too far west)", !bboxGeom.intersects(outsideWest)); + assertTrue("Point at -160° should be OUTSIDE (too far east)", !bboxGeom.intersects(outsideEast)); + assertTrue("Point at -20° latitude should be OUTSIDE (too far north)", !bboxGeom.intersects(outsideNorth)); + assertTrue("Point at -60° latitude should be OUTSIDE (too far south)", !bboxGeom.intersects(outsideSouth)); + } + + /** + * Test a normal (non-antimeridian) bbox in the same region + * This should create a single Polygon, not a MultiPolygon + */ + @Test + public void testNormalBboxInSameRegion() { + GetFeatureRequest req = new GetFeatureRequest(); + GetFeatureCollection getItems = new GetFeatureCollection(collection); + req.addCollection(getItems); + + // Normal bbox: minX < maxX (165 to 175, does not cross antimeridian) + new BboxParam().modify(service, req, "165,-50,175,-30"); + + Filter filter = getItems.getFilters().get(0); + Geometry geom = (Geometry) filter.getValue(); + + assertEquals(84, geom.getSRID()); + assertEquals("Polygon", geom.getGeometryType()); + + // Should intersect western points + Point westPoint = HakunaGeometryFactory.GF.createPoint(new Coordinate(170.0, -35.0)); + assertTrue("Normal bbox should intersect point at 170°", geom.intersects(westPoint)); + } + + /** + * Test bbox that spans exactly from -180 to 180 (full longitude range) + */ + @Test + public void testFullLongitudeRangeBbox() { + GetFeatureRequest req = new GetFeatureRequest(); + GetFeatureCollection getItems = new GetFeatureCollection(collection); + req.addCollection(getItems); + + new BboxParam().modify(service, req, "-180,-90,180,90"); + + Filter filter = getItems.getFilters().get(0); + Geometry geom = (Geometry) filter.getValue(); + + assertEquals(84, geom.getSRID()); + + // Should be a single polygon covering the whole world + Point anyPoint = HakunaGeometryFactory.GF.createPoint(new Coordinate(0, 0)); + assertTrue("Full range bbox should intersect any point", geom.intersects(anyPoint)); + } + + /** + * Test that antimeridian bbox works with EPSG:4326 (lat/lon order) + * After axis swapping, BboxParam should still detect antimeridian crossing + */ + @Test + public void testAntimeridianBboxWithEPSG4326() { + GetFeatureRequest req = new GetFeatureRequest(); + GetFeatureCollection getItems = new GetFeatureCollection(collection); + req.addCollection(getItems); + + String bboxCrs = "http://www.opengis.net/def/crs/EPSG/0/4326"; + new BboxCrsParam().modify(service, req, bboxCrs); + // EPSG:4326 uses lat/lon order in the input + new BboxParam().modify(service, req, "-55.95,160.6,-25.89,-170"); + + Filter filter = getItems.getFilters().get(0); + Geometry geom = (Geometry) filter.getValue(); + + // Should have SRID 4326 and create a geometry (possibly MultiPolygon after reprojection) + assertEquals(4326, geom.getSRID()); + assertEquals("MultiPolygon", geom.getGeometryType()); + assertEquals(2, geom.getNumGeometries()); + } + + /** + * Test bbox close to but not crossing antimeridian (179° to 180°) + */ + @Test + public void testBboxNearAntimeridianNotCrossing() { + GetFeatureRequest req = new GetFeatureRequest(); + GetFeatureCollection getItems = new GetFeatureCollection(collection); + req.addCollection(getItems); + + new BboxParam().modify(service, req, "179,-10,180,10"); + + Filter filter = getItems.getFilters().get(0); + Geometry geom = (Geometry) filter.getValue(); + + // Should be a single polygon since it doesn't cross + assertEquals("Polygon", geom.getGeometryType()); + } + + /** + * Test very narrow bbox crossing antimeridian (179.5° to -179.5°) + */ + @Test + public void testNarrowAntimeridianCrossingBbox() { + GetFeatureRequest req = new GetFeatureRequest(); + GetFeatureCollection getItems = new GetFeatureCollection(collection); + req.addCollection(getItems); + + new BboxParam().modify(service, req, "179.5,-10,-179.5,10"); + + Filter filter = getItems.getFilters().get(0); + Geometry geom = (Geometry) filter.getValue(); + + assertEquals("MultiPolygon", geom.getGeometryType()); + + // Should intersect points very close to antimeridian on both sides + Point westOfLine = HakunaGeometryFactory.GF.createPoint(new Coordinate(179.8, 0)); + Point eastOfLine = HakunaGeometryFactory.GF.createPoint(new Coordinate(-179.8, 0)); + + assertTrue("Should intersect point just west of antimeridian", geom.intersects(westOfLine)); + assertTrue("Should intersect point just east of antimeridian", geom.intersects(eastOfLine)); + } +} diff --git a/src/hakunapi-core/src/test/java/fi/nls/hakunapi/core/param/BboxCrsParamTest.java b/src/hakunapi-core/src/test/java/fi/nls/hakunapi/core/param/BboxCrsParamTest.java index de8aa337..d312c0f5 100644 --- a/src/hakunapi-core/src/test/java/fi/nls/hakunapi/core/param/BboxCrsParamTest.java +++ b/src/hakunapi-core/src/test/java/fi/nls/hakunapi/core/param/BboxCrsParamTest.java @@ -20,6 +20,9 @@ import fi.nls.hakunapi.core.SRIDCode; import fi.nls.hakunapi.core.SimpleFeatureType; import fi.nls.hakunapi.core.filter.Filter; +import fi.nls.hakunapi.core.projection.NOPProjectionTransformer; +import fi.nls.hakunapi.core.projection.ProjectionTransformer; +import fi.nls.hakunapi.core.projection.ProjectionTransformerFactory; import fi.nls.hakunapi.core.property.simple.HakunaPropertyGeometry; import fi.nls.hakunapi.core.request.GetFeatureCollection; import fi.nls.hakunapi.core.request.GetFeatureRequest; @@ -44,6 +47,20 @@ public FeatureProducer getFeatureProducer() { myCollection.setStaticFilters(Collections.emptyList()); myCollection.setGeom(new HakunaPropertyGeometry("geometry", "table", null, false, null, new int[0], 84, 2, (__, ___, ____) -> {})); myCollection.setPaginationStrategy(PaginationStrategyOffset.INSTANCE); + myCollection.setProjectionTransformerFactory(new ProjectionTransformerFactory() { + @Override + public ProjectionTransformer getTransformer(int fromSRID, int toSRID) { + return NOPProjectionTransformer.INSTANCE; + } + @Override + public ProjectionTransformer toCRS84(int fromSRID) { + return NOPProjectionTransformer.INSTANCE; + } + @Override + public ProjectionTransformer fromCRS84(int toSRID) { + return NOPProjectionTransformer.INSTANCE; + } + }); service = new FeatureServiceConfig() { @@ -77,8 +94,8 @@ public void testLonLatRS84() { GetFeatureCollection getItems = new GetFeatureCollection(myCollection); req.addCollection(getItems); - new BboxParam().modify(service, req, "-180,-90,180,90"); new BboxCrsParam().modify(service, req, Crs.CRS84); + new BboxParam().modify(service, req, "-180,-90,180,90"); List filters = getItems.getFilters(); assertEquals(1, filters.size()); @@ -96,9 +113,9 @@ public void testLatLonCRS4326() { GetFeatureCollection getItems = new GetFeatureCollection(myCollection); req.addCollection(getItems); - new BboxParam().modify(service, req, "-90,-180,90,180"); String bboxCrs = "http://www.opengis.net/def/crs/EPSG/0/4326"; new BboxCrsParam().modify(service, req, bboxCrs); + new BboxParam().modify(service, req, "-90,-180,90,180"); List filters = getItems.getFilters(); assertEquals(1, filters.size()); @@ -110,4 +127,25 @@ public void testLatLonCRS4326() { assertEquals(-90, bottomLeft.getY(), 0.0); } + @Test + public void testAntimeridianBboxCRS84() { + GetFeatureRequest req = new GetFeatureRequest(); + GetFeatureCollection getItems = new GetFeatureCollection(myCollection); + req.addCollection(getItems); + + new BboxCrsParam().modify(service, req, Crs.CRS84); + new BboxParam().modify(service, req, "160.6,-55.95,-170,-25.89"); + + List filters = getItems.getFilters(); + assertEquals(1, filters.size()); + Filter filter = filters.get(0); + Geometry geom = (Geometry) filter.getValue(); + assertEquals(84, geom.getSRID()); + assertEquals("MultiPolygon", geom.getGeometryType()); + assertEquals(2, geom.getNumGeometries()); + + Coordinate[] coords = geom.getCoordinates(); + assertEquals(10, coords.length); + } + } diff --git a/src/hakunapi-cql2/src/main/java/fi/nls/hakunapi/cql2/model/ExpressionToHakunaFilter.java b/src/hakunapi-cql2/src/main/java/fi/nls/hakunapi/cql2/model/ExpressionToHakunaFilter.java index 9891cb47..ac27c8f0 100644 --- a/src/hakunapi-cql2/src/main/java/fi/nls/hakunapi/cql2/model/ExpressionToHakunaFilter.java +++ b/src/hakunapi-cql2/src/main/java/fi/nls/hakunapi/cql2/model/ExpressionToHakunaFilter.java @@ -7,7 +7,10 @@ import org.locationtech.jts.geom.Geometry; import fi.nls.hakunapi.core.FeatureType; +import fi.nls.hakunapi.core.SRIDCode; import fi.nls.hakunapi.core.filter.Filter; +import fi.nls.hakunapi.core.geom.Bbox; +import fi.nls.hakunapi.core.projection.ProjectionHelper; import fi.nls.hakunapi.core.property.HakunaProperty; import fi.nls.hakunapi.core.property.simple.HakunaPropertyGeometry; import fi.nls.hakunapi.cql2.function.CQL2Functions; @@ -28,10 +31,12 @@ public class ExpressionToHakunaFilter implements ExpressionVisitor { private final Map queryables; + private final SRIDCode filterSrid; - public ExpressionToHakunaFilter(FeatureType ft) { + public ExpressionToHakunaFilter(FeatureType ft, SRIDCode filterSrid) { this.queryables = ft.getQueryableProperties().stream() .collect(Collectors.toMap(HakunaProperty::getName, it -> it)); + this.filterSrid = filterSrid; } @Override @@ -141,6 +146,19 @@ public Object visit(SpatialPredicate p) { } Geometry geom = (Geometry) geometry; + // Check if this is a bbox that crosses the wrap-x boundary (antimeridian) + if (Bbox.isWrapXCrossingBbox(geom)) { + if (filterSrid.supportsWrapX()) { + geom = Bbox.splitWrapXBbox(geom, filterSrid.getWrapXMin(), filterSrid.getWrapXMax()); + } else { + throw new IllegalArgumentException( + "Invalid bbox: west-most edge is larger than east-most edge. " + + "This is only valid for coordinate systems that support wrap-x."); + } + } + + geom = ProjectionHelper.reprojectToStorageCRS(prop, geom); + switch (p.getOp()) { case S_INTERSECTS: return Filter.intersects(prop, geom); diff --git a/src/hakunapi-cql2/src/main/java/fi/nls/hakunapi/cql2/text/CQL2Text.java b/src/hakunapi-cql2/src/main/java/fi/nls/hakunapi/cql2/text/CQL2Text.java index ae31a570..3b2d5c05 100644 --- a/src/hakunapi-cql2/src/main/java/fi/nls/hakunapi/cql2/text/CQL2Text.java +++ b/src/hakunapi-cql2/src/main/java/fi/nls/hakunapi/cql2/text/CQL2Text.java @@ -9,6 +9,7 @@ import fi.nls.hakunapi.core.FeatureType; import fi.nls.hakunapi.core.FilterParser; +import fi.nls.hakunapi.core.SRIDCode; import fi.nls.hakunapi.core.filter.Filter; import fi.nls.hakunapi.cql2.Cql2Lexer; import fi.nls.hakunapi.cql2.Cql2Parser; @@ -28,10 +29,10 @@ public String getCode() { } @Override - public Filter parse(FeatureType ft, String filter, int filterSrid) throws IllegalArgumentException { + public Filter parse(FeatureType ft, String filter, SRIDCode filterSrid) throws IllegalArgumentException { try { - Expression expression = parse(filter, new GeometryFactory(new PrecisionModel(), filterSrid)); - return (Filter) new ExpressionToHakunaFilter(ft).visit(expression); + Expression expression = parse(filter, new GeometryFactory(new PrecisionModel(), filterSrid.getSrid())); + return (Filter) new ExpressionToHakunaFilter(ft, filterSrid).visit(expression); } catch (Exception e) { if (e instanceof IllegalArgumentException) { throw e; diff --git a/src/hakunapi-cql2/src/main/java/fi/nls/hakunapi/cql2/text/TextParserVisitor.java b/src/hakunapi-cql2/src/main/java/fi/nls/hakunapi/cql2/text/TextParserVisitor.java index 186e2429..7b86bde2 100644 --- a/src/hakunapi-cql2/src/main/java/fi/nls/hakunapi/cql2/text/TextParserVisitor.java +++ b/src/hakunapi-cql2/src/main/java/fi/nls/hakunapi/cql2/text/TextParserVisitor.java @@ -13,6 +13,7 @@ import org.locationtech.jts.geom.Point; import org.locationtech.jts.geom.Polygon; +import fi.nls.hakunapi.core.geom.Bbox; import fi.nls.hakunapi.cql2.Cql2Parser; import fi.nls.hakunapi.cql2.Cql2Parser.BinaryComparisonPredicateContext; import fi.nls.hakunapi.cql2.Cql2Parser.PropertyNameContext; @@ -332,18 +333,11 @@ public Expression visitBboxLiteral(Cql2Parser.BboxLiteralContext ctx) { if (values.length == 6) { // Skip z i++; - // Currently we throw an exception with ?bbox=x1,y1,z1,x2,y2,z2 so this is not exact same behaviour } double x2 = values[i++]; double y2 = values[i++]; - Coordinate[] shell = new Coordinate[] { - new Coordinate(x1, y1), - new Coordinate(x2, y1), - new Coordinate(x2, y2), - new Coordinate(x1, y2), - new Coordinate(x1, y1) - }; - return new SpatialLiteral(gf.createPolygon(shell)); + Geometry bbox = Bbox.createBboxGeometry(x1, y1, x2, y2, gf); + return new SpatialLiteral(bbox); } @Override diff --git a/src/hakunapi-cql2/src/test/java/fi/nls/hakunapi/cql2/text/TextParserVisitorTest.java b/src/hakunapi-cql2/src/test/java/fi/nls/hakunapi/cql2/text/TextParserVisitorTest.java index c345ec04..48a20ea0 100644 --- a/src/hakunapi-cql2/src/test/java/fi/nls/hakunapi/cql2/text/TextParserVisitorTest.java +++ b/src/hakunapi-cql2/src/test/java/fi/nls/hakunapi/cql2/text/TextParserVisitorTest.java @@ -259,9 +259,9 @@ public void testCrossesPolygon() { @Test public void testSIntersectsBbox() { - String intersects_envelope = "S_Intersects(footprint, bbox (43.5845, -79.5442, 43.6079, -79.7893))"; + String intersects_envelope = "S_Intersects(footprint, bbox (43.5845, -79.7893, 43.6079, -79.5442))"; parse(intersects_envelope).accept(toString); - String expected = "S_INTERSECTS(footprint, POLYGON ((43.5845 -79.5442, 43.6079 -79.5442, 43.6079 -79.7893, 43.5845 -79.7893, 43.5845 -79.5442)))"; + String expected = "S_INTERSECTS(footprint, POLYGON ((43.5845 -79.7893, 43.6079 -79.7893, 43.6079 -79.5442, 43.5845 -79.5442, 43.5845 -79.7893)))"; String actual = toString.finish(); assertEquals(expected, actual); } diff --git a/src/hakunapi-crs-properties/src/main/java/fi/nls/hakunapi/crs/properties/PropertiesCRSRegistry.java b/src/hakunapi-crs-properties/src/main/java/fi/nls/hakunapi/crs/properties/PropertiesCRSRegistry.java index 39560d5d..0b4ece0c 100644 --- a/src/hakunapi-crs-properties/src/main/java/fi/nls/hakunapi/crs/properties/PropertiesCRSRegistry.java +++ b/src/hakunapi-crs-properties/src/main/java/fi/nls/hakunapi/crs/properties/PropertiesCRSRegistry.java @@ -12,6 +12,8 @@ * srid..latLon=true/false (default false) * srid..degrees=true/false (default false) * srid..geometryDimension=XY/XYZ/XYZM (default XY) + * srid..wrapXMin= (optional, e.g., -180.0 for geographic, -20037508 for WebMercator) + * srid..wrapXMax= (optional, e.g., 180.0 for geographic, 20037508 for WebMercator) */ public class PropertiesCRSRegistry implements CRSRegistry { @@ -20,12 +22,30 @@ public SRIDCode detect(Properties config, int srid) { String latLonStr = config.getProperty("srid." + srid + ".latLon", "false"); String degreesStr = config.getProperty("srid." + srid + ".degrees", "false"); String dimensionStr = config.getProperty("srid." + srid + ".geometryDimension", "XY"); + String wrapXMinStr = config.getProperty("srid." + srid + ".wrapXMin"); + String wrapXMaxStr = config.getProperty("srid." + srid + ".wrapXMax"); boolean latLon = "true".equalsIgnoreCase(latLonStr); boolean degrees = "true".equalsIgnoreCase(degreesStr); - HakunaGeometryDimension geomDimension = HakunaGeometryDimension.valueOf(dimensionStr); + HakunaGeometryDimension geomDimension = HakunaGeometryDimension.valueOf(dimensionStr); - return new SRIDCode(srid, latLon, degrees, geomDimension); + Double wrapXMin = parseDouble(wrapXMinStr); + Double wrapXMax = parseDouble(wrapXMaxStr); + + // Validate: both must be set or neither + if ((wrapXMin == null) != (wrapXMax == null)) { + throw new IllegalArgumentException( + "srid." + srid + ": Both wrapXMin and wrapXMax must be set, or neither"); + } + + return new SRIDCode(srid, latLon, degrees, geomDimension, wrapXMin, wrapXMax); + } + + private static Double parseDouble(String value) { + if (value == null || value.isEmpty()) { + return null; + } + return Double.parseDouble(value); } } diff --git a/src/hakunapi-source-gpkg/src/main/java/fi/nls/hakunapi/source/gpkg/filter/GpkgIntersects.java b/src/hakunapi-source-gpkg/src/main/java/fi/nls/hakunapi/source/gpkg/filter/GpkgIntersects.java index 9f761d5c..2f416a21 100644 --- a/src/hakunapi-source-gpkg/src/main/java/fi/nls/hakunapi/source/gpkg/filter/GpkgIntersects.java +++ b/src/hakunapi-source-gpkg/src/main/java/fi/nls/hakunapi/source/gpkg/filter/GpkgIntersects.java @@ -8,7 +8,6 @@ import org.locationtech.jts.geom.Geometry; import fi.nls.hakunapi.core.filter.Filter; -import fi.nls.hakunapi.core.projection.ProjectionHelper; import fi.nls.hakunapi.core.property.simple.HakunaPropertyGeometry; import fi.nls.hakunapi.source.gpkg.GpkgFeatureType; @@ -50,8 +49,7 @@ public String toSQL(Filter filter) { @Override public int bind(Filter filter, Connection c, PreparedStatement ps, int i) throws SQLException { - HakunaPropertyGeometry prop = (HakunaPropertyGeometry) filter.getProp(); - Geometry geom = ProjectionHelper.reprojectToStorageCRS(prop, (Geometry) filter.getValue()); + Geometry geom = (Geometry) filter.getValue(); Envelope envelope = geom.getEnvelopeInternal(); ps.setDouble(i++, envelope.getMinX()); diff --git a/src/hakunapi-source-oracle/src/main/java/fi/nls/hakunapi/simple/sdo/filter/SDOGeometryFunction.java b/src/hakunapi-source-oracle/src/main/java/fi/nls/hakunapi/simple/sdo/filter/SDOGeometryFunction.java index bb9c49e2..b22d6f50 100644 --- a/src/hakunapi-source-oracle/src/main/java/fi/nls/hakunapi/simple/sdo/filter/SDOGeometryFunction.java +++ b/src/hakunapi-source-oracle/src/main/java/fi/nls/hakunapi/simple/sdo/filter/SDOGeometryFunction.java @@ -8,7 +8,6 @@ import org.locationtech.jts.geom.Geometry; import fi.nls.hakunapi.core.filter.Filter; -import fi.nls.hakunapi.core.projection.ProjectionHelper; import fi.nls.hakunapi.core.property.HakunaProperty; import fi.nls.hakunapi.core.property.simple.HakunaPropertyGeometry; import fi.nls.hakunapi.simple.sdo.sql.filter.SQLFilter; @@ -37,7 +36,6 @@ public String toSQL(Filter filter) { public int bind(Filter filter, Connection c, PreparedStatement ps, int i) throws SQLException { HakunaPropertyGeometry prop = (HakunaPropertyGeometry) filter.getProp(); Geometry geom = (Geometry) filter.getValue(); - geom = ProjectionHelper.reprojectToStorageCRS(prop, geom); OracleConnection oc = c.unwrap(OracleConnection.class); diff --git a/src/hakunapi-source-oracle/src/main/java/fi/nls/hakunapi/simple/sdo/filter/SDOIntersectsIndex.java b/src/hakunapi-source-oracle/src/main/java/fi/nls/hakunapi/simple/sdo/filter/SDOIntersectsIndex.java index 554eab8f..8e71c058 100644 --- a/src/hakunapi-source-oracle/src/main/java/fi/nls/hakunapi/simple/sdo/filter/SDOIntersectsIndex.java +++ b/src/hakunapi-source-oracle/src/main/java/fi/nls/hakunapi/simple/sdo/filter/SDOIntersectsIndex.java @@ -8,7 +8,6 @@ import org.locationtech.jts.geom.Geometry; import fi.nls.hakunapi.core.filter.Filter; -import fi.nls.hakunapi.core.projection.ProjectionHelper; import fi.nls.hakunapi.core.property.HakunaProperty; import fi.nls.hakunapi.core.property.simple.HakunaPropertyGeometry; import fi.nls.hakunapi.simple.sdo.sql.filter.SQLFilter; @@ -34,7 +33,6 @@ public String toSQL(Filter filter) { public int bind(Filter filter, Connection c, PreparedStatement ps, int i) throws SQLException { HakunaPropertyGeometry prop = (HakunaPropertyGeometry) filter.getProp(); Geometry geom = (Geometry) filter.getValue(); - geom = ProjectionHelper.reprojectToStorageCRS(prop, geom); OracleConnection oc = c.unwrap(OracleConnection.class); diff --git a/src/hakunapi-source-postgis/src/main/java/fi/nls/hakunapi/simple/postgis/filter/PostGISGeometryFunction.java b/src/hakunapi-source-postgis/src/main/java/fi/nls/hakunapi/simple/postgis/filter/PostGISGeometryFunction.java index 46e7c07c..48a9e5f7 100644 --- a/src/hakunapi-source-postgis/src/main/java/fi/nls/hakunapi/simple/postgis/filter/PostGISGeometryFunction.java +++ b/src/hakunapi-source-postgis/src/main/java/fi/nls/hakunapi/simple/postgis/filter/PostGISGeometryFunction.java @@ -8,13 +8,11 @@ import org.locationtech.jts.io.WKBWriter; import fi.nls.hakunapi.core.filter.Filter; -import fi.nls.hakunapi.core.projection.ProjectionHelper; import fi.nls.hakunapi.core.property.HakunaProperty; -import fi.nls.hakunapi.core.property.simple.HakunaPropertyGeometry; public abstract class PostGISGeometryFunction implements SQLFilter { - - public abstract String getFunctionName(); + + public abstract String getFunctionName(); @Override public String toSQL(Filter filter) { @@ -25,9 +23,7 @@ public String toSQL(Filter filter) { @Override public int bind(Filter filter, Connection c, PreparedStatement ps, int i) throws SQLException { - HakunaPropertyGeometry prop = (HakunaPropertyGeometry) filter.getProp(); Geometry geom = (Geometry) filter.getValue(); - geom = ProjectionHelper.reprojectToStorageCRS(prop, geom); int outputDimension = geom.getDimension() > 2 ? 3 : 2; byte[] wkb = new WKBWriter(outputDimension, false).write(geom); diff --git a/src/hakunapi-source-postgis/src/main/java/fi/nls/hakunapi/simple/postgis/filter/PostGISIntersectsIndex.java b/src/hakunapi-source-postgis/src/main/java/fi/nls/hakunapi/simple/postgis/filter/PostGISIntersectsIndex.java index 5ae63836..52490a49 100644 --- a/src/hakunapi-source-postgis/src/main/java/fi/nls/hakunapi/simple/postgis/filter/PostGISIntersectsIndex.java +++ b/src/hakunapi-source-postgis/src/main/java/fi/nls/hakunapi/simple/postgis/filter/PostGISIntersectsIndex.java @@ -8,7 +8,6 @@ import org.locationtech.jts.io.WKBWriter; import fi.nls.hakunapi.core.filter.Filter; -import fi.nls.hakunapi.core.projection.ProjectionHelper; import fi.nls.hakunapi.core.property.HakunaProperty; import fi.nls.hakunapi.core.property.simple.HakunaPropertyGeometry; @@ -23,9 +22,7 @@ public String toSQL(Filter filter) { @Override public int bind(Filter filter, Connection c, PreparedStatement ps, int i) throws SQLException { - HakunaPropertyGeometry prop = (HakunaPropertyGeometry) filter.getProp(); Geometry bbox = (Geometry) filter.getValue(); - bbox = ProjectionHelper.reprojectToStorageCRS(prop, bbox); int outputDimension = bbox.getDimension() > 2 ? 3 : 2; byte[] wkb = new WKBWriter(outputDimension, false).write(bbox);