diff --git a/core/src/main/java/org/fao/geonet/kernel/search/spatial/ErrorHandler.java b/core/src/main/java/org/fao/geonet/kernel/search/spatial/ErrorHandler.java index b7065892101..c711bbfe5ef 100644 --- a/core/src/main/java/org/fao/geonet/kernel/search/spatial/ErrorHandler.java +++ b/core/src/main/java/org/fao/geonet/kernel/search/spatial/ErrorHandler.java @@ -2,10 +2,11 @@ import java.util.List; +import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.Polygon; public interface ErrorHandler { void handleParseException(Exception e, String gml); - void handleBuildException(Exception e, List polygons); + void handleBuildException(Exception e, List geometries); } diff --git a/services/src/main/java/org/fao/geonet/api/regions/metadata/GeomUtils.java b/services/src/main/java/org/fao/geonet/api/regions/metadata/GeomUtils.java index 88d24fa487c..f926338c9ef 100644 --- a/services/src/main/java/org/fao/geonet/api/regions/metadata/GeomUtils.java +++ b/services/src/main/java/org/fao/geonet/api/regions/metadata/GeomUtils.java @@ -10,10 +10,8 @@ import org.geotools.referencing.crs.DefaultGeographicCRS; import org.geotools.xsd.Parser; import org.jdom.Element; -import org.locationtech.jts.geom.Geometry; -import org.locationtech.jts.geom.GeometryFactory; -import org.locationtech.jts.geom.MultiPolygon; -import org.locationtech.jts.geom.Polygon; +import org.locationtech.jts.geom.*; +import org.locationtech.jts.geom.util.GeometryTransformer; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.operation.MathTransform; import org.xml.sax.SAXException; @@ -28,8 +26,12 @@ import java.util.List; import java.util.logging.Level; +/** + * Utility class to help with geometry and gml. + */ public class GeomUtils { - public static MultiPolygon getSpatialExtent(Path schemaDir, Element metadata, ErrorHandler errorHandler) throws Exception { + + public static Geometry getSpatialExtent(Path schemaDir, Element metadata, ErrorHandler errorHandler) throws Exception { org.geotools.util.logging.Logging.getLogger("org.geotools.xml") .setLevel(Level.SEVERE); Path sSheet = schemaDir.resolve("extract-gml.xsl").toAbsolutePath(); @@ -37,7 +39,7 @@ public static MultiPolygon getSpatialExtent(Path schemaDir, Element metadata, Er if (transform.getChildren().size() == 0) { return null; } - List allPolygons = new ArrayList(); + List allGeometry = new ArrayList(); for (Element geom : (List) transform.getChildren()) { Parser parser = GMLParsers.create(geom); String srs = geom.getAttributeValue("srsName"); @@ -46,91 +48,265 @@ public static MultiPolygon getSpatialExtent(Path schemaDir, Element metadata, Er try { if (srs != null && !(srs.equals(""))) sourceCRS = CRS.decode(srs); - MultiPolygon jts = parseGml(parser, gml); + Geometry geometry = parseGml(parser, gml); // if we have an srs and its not WGS84 then transform to WGS84 if (!CRS.equalsIgnoreMetadata(sourceCRS, DefaultGeographicCRS.WGS84)) { MathTransform tform = CRS.findMathTransform(sourceCRS, DefaultGeographicCRS.WGS84); - jts = (MultiPolygon) JTS.transform(jts, tform); - } - - for (int i = 0; i < jts.getNumGeometries(); i++) { - allPolygons.add((Polygon) jts.getGeometryN(i)); + geometry = JTS.transform(geometry, tform); } + allGeometry.add(geometry); } catch (Exception e) { errorHandler.handleParseException(e, gml); // continue } } - if (allPolygons.isEmpty()) { + try { + return toGeometry(allGeometry); + } catch (Exception e) { + errorHandler.handleBuildException(e, allGeometry); + return null; // continue + } + } + + /** + * Process list into a single geometry, choosing the most appropriate geometry collection. + * + *
    + *
  • f the list is empty geometry is {@code null}
  • + *
  • If list contains a single geometry it is returned
  • + *
  • If the list contains all Points, a MultiPoint will be created
  • + *
  • If the list contains all LineStrings a MultiLineString will be created
  • + *
  • If the list contains all Polygons a MultiPolygon is created
  • + *
  • If the list contains mixed contents a GeometryCollection is created
  • + *
+ * + * @param geoms list of geometry to process + * @return geometry, or geometry collection as required, or null if list is empty + */ + protected static Geometry toGeometry(List geoms){ + if( geoms == null || geoms.isEmpty()){ return null; - } else { - try { - Polygon[] array = new Polygon[allPolygons.size()]; - GeometryFactory geometryFactory = allPolygons.get(0).getFactory(); - return geometryFactory.createMultiPolygon(allPolygons.toArray(array)); + } + if( geoms.size()==1 ){ + return geoms.get(0); + } + GeometryFactory factory = geoms.get(0).getFactory(); - } catch (Exception e) { - errorHandler.handleBuildException(e, allPolygons); - // continue - return null; + // determine geometry collection to create + int dimension = -2; // mixed content geometry collection + for( Geometry geom : geoms){ + if( geom instanceof GeometryCollection){ + dimension = -1; + break; // mixed content geometry collection + } + + if( dimension == -2 ) { + dimension = geom.getDimension(); } + else if (dimension != geom.getDimension()){ + dimension = -1; + break; // mixed content geometry collection + } + } + + // process list into geometry collection + switch (dimension){ + case 0: + return factory.createMultiPoint( geoms.toArray(new Point[geoms.size()])); + case 1: + return factory.createMultiLineString( geoms.toArray(new LineString[geoms.size()]) ); + case 2: + return factory.createMultiPolygon( geoms.toArray(new Polygon[geoms.size()]) ); + default: + return factory.createGeometryCollection( geoms.toArray(new Geometry[geoms.size()]) ); } } - public static MultiPolygon parseGml(Parser parser, String gml) throws IOException, SAXException, + /** + * Parse GML into a geometry: polygons, linestring, point or appropriate geometry collection as required. + * + * The resulting geometry may be clipped or split to accommodate spatial reference system bounds. Points are not buffered + * in any way (so the bounds of a point will have width and height zero). + * + * @param parser + * @param gml + * @return geometry, or null if not provided. + * @throws IOException + * @throws SAXException + * @throws ParserConfigurationException + */ + public static Geometry parseGml(Parser parser, String gml) throws IOException, SAXException, ParserConfigurationException { Object value = parser.parse(new StringReader(gml)); - if (value instanceof HashMap) { + + if( value == null ){ + return null; + } + else if (value instanceof HashMap) { @SuppressWarnings("rawtypes") HashMap map = (HashMap) value; - List geoms = new ArrayList(); + List geoms = new ArrayList(); for (Object entry : map.values()) { addToList(geoms, entry); } - if (geoms.isEmpty()) { - return null; - } else if (geoms.size() > 1) { - GeometryFactory factory = geoms.get(0).getFactory(); - return factory.createMultiPolygon(geoms.toArray(new Polygon[0])); - } else { - return toMultiPolygon(geoms.get(0)); - } - - } else if (value == null) { + return toGeometry( geoms ); + } else if (value instanceof Geometry ){ + return (Geometry) value; + } + else { return null; - } else { - return toMultiPolygon((Geometry) value); } } + /** + * Produces a multipolygon that covers the provided geometry. + * + * @param geometry + * @return covering multipolygon + */ public static MultiPolygon toMultiPolygon(Geometry geometry) { - if (geometry instanceof Polygon) { - Polygon polygon = (Polygon) geometry; - - return geometry.getFactory().createMultiPolygon( - new Polygon[]{polygon}); - } else if (geometry instanceof MultiPolygon) { + if (geometry == null) return null; + if (geometry instanceof MultiPolygon) { return (MultiPolygon) geometry; } - String message = geometry.getClass() + " cannot be converted to a polygon. Check metadata"; - Log.error(Geonet.INDEX_ENGINE, message); - throw new IllegalArgumentException(message); + final double DISTANCE = 0.01; + GeometryTransformer transform = new CoveredByTransformer(DISTANCE); + + Geometry transformed = transform.transform(geometry); + if( transformed == null ){ + String message = geometry.getClass() + " cannot be converted to a polygon. Check metadata"; + Log.error(Geonet.INDEX_ENGINE, message); + throw new IllegalArgumentException(message); + } + transformed.setSRID(geometry.getSRID()); + transformed.setUserData(geometry.getUserData()); + + if( transformed instanceof MultiPolygon) { + return (MultiPolygon) transformed; + } + else { + return null; + } } - public static void addToList(List geoms, Object entry) { - if (entry instanceof Polygon) { - geoms.add(toMultiPolygon((Polygon) entry)); - } else if (entry instanceof MultiPolygon) { - geoms.add((MultiPolygon) entry); + /** + * Process entry and add any geometries to list. + * + * The contents of any GeometryCollection (such as MultiPolygon) are added one-by-one + * by one to the list. + */ + protected static final void addToList(List geoms, Object entry) { + if (entry instanceof Geometry) { + Geometry geom = (Geometry) entry; + if( geom instanceof GeometryCollection){ + GeometryCollection collection = (GeometryCollection) geom; + for( int i=0; i transGeomList = new ArrayList<>(); + for (int i = 0; i < geom.getNumGeometries(); i++) { + Geometry transformGeom = transform(geom.getGeometryN(i)); + if (transformGeom == null) continue; + if (transformGeom.isEmpty()) continue; + + if( transformGeom instanceof MultiPolygon) { + MultiPolygon transformedMultiPolygon = (MultiPolygon) transformGeom; + for (int j = 0; j < transformedMultiPolygon.getNumGeometries(); j++) { + Polygon polygon = (Polygon) transformedMultiPolygon.getGeometryN(j); + if (polygon == null) continue; + if (polygon.isEmpty()) continue; + transGeomList.add(polygon); + } + } + else if (transformGeom instanceof Polygon){ + transGeomList.add((Polygon)transformGeom); + } } + return factory.createMultiPolygon(GeometryFactory.toPolygonArray(transGeomList)); } } diff --git a/services/src/main/java/org/fao/geonet/api/regions/metadata/MetadataRegionSearchRequest.java b/services/src/main/java/org/fao/geonet/api/regions/metadata/MetadataRegionSearchRequest.java index 156f68ed56c..13fe6c13a31 100644 --- a/services/src/main/java/org/fao/geonet/api/regions/metadata/MetadataRegionSearchRequest.java +++ b/services/src/main/java/org/fao/geonet/api/regions/metadata/MetadataRegionSearchRequest.java @@ -163,7 +163,7 @@ private void loadSpatialExtent(List regions, Id id) throws Exception { Element metadata = findMetadata(id, false); if (metadata != null) { Path schemaDir = getSchemaDir(id); - MultiPolygon geom = GeomUtils.getSpatialExtent(schemaDir, metadata, + Geometry geom = GeomUtils.getSpatialExtent(schemaDir, metadata, new SpatialExtentErrorHandler()); MetadataRegion region = new MetadataRegion(id, null, geom); regions.add(region); @@ -370,8 +370,8 @@ public void handleParseException(Exception e, String gml) { } @Override - public void handleBuildException(Exception e, List polygons) { - Log.error(Geonet.SPATIAL, "Failed to create a MultiPolygon from: " + polygons, e); + public void handleBuildException(Exception e, List geoms) { + Log.error(Geonet.SPATIAL, "Failed to create a geometry from: " + geoms, e); } } } diff --git a/services/src/test/java/org/fao/geonet/api/regions/metadata/GeomUtilsTest.java b/services/src/test/java/org/fao/geonet/api/regions/metadata/GeomUtilsTest.java new file mode 100644 index 00000000000..c867b0ac54c --- /dev/null +++ b/services/src/test/java/org/fao/geonet/api/regions/metadata/GeomUtilsTest.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2001-2016 Food and Agriculture Organization of the + * United Nations (FAO-UN), United Nations World Food Programme (WFP) + * and United Nations Environment Programme (UNEP) + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + * + * Contact: Jeroen Ticheler - FAO - Viale delle Terme di Caracalla 2, + * Rome - Italy. email: geonetwork@osgeo.org + */ +package org.fao.geonet.api.regions.metadata; + +import org.geotools.geometry.jts.JTSFactoryFinder; +import org.geotools.util.factory.Hints; +import org.junit.Test; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryCollection; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.MultiPolygon; +import org.locationtech.jts.io.WKTReader; + +import static org.junit.Assert.*; +public class GeomUtilsTest { + protected GeometryFactory factory = JTSFactoryFinder.getGeometryFactory(new Hints(Hints.JTS_SRID, 4326)); + + @Test + public void point() throws Exception { + checkBounds("POINT(0 0)"); + } + @Test + public void points() throws Exception { + checkBounds("MULTIPOINT(0 0,1 0,1 1,0 1,0 0)"); + } + @Test + public void line() throws Exception { + checkBounds("LINESTRING(0 0,1 1)"); + } + @Test + public void ring() throws Exception { + checkBounds("LINEARRING(0 0,1 0,1 1,0 1,0 0)"); + } + @Test + public void lines() throws Exception { + checkBounds("MULTILINESTRING ((10 10, 20 20, 10 40),(40 40, 30 30, 40 20, 30 10))"); + } + @Test + public void polygon() throws Exception { + checkBounds("POLYGON((0 0,1 0,1 1,0 1,0 0))"); + checkBounds("POLYGON ((35 10, 45 45, 15 40, 10 20, 35 10),(20 30, 35 35, 30 20, 20 30))"); + } + @Test + public void polygons() throws Exception { + checkBounds("MULTIPOLYGON (((30 20, 45 40, 10 40, 30 20)),((15 5, 40 10, 10 20, 5 10, 15 5)))"); + } + @Test + public void collection() throws Exception { + checkBounds( "GEOMETRYCOLLECTION (POINT (40 10),LINESTRING (10 10, 20 20, 10 40),POLYGON ((40 40, 20 45, 45 30, 40 40)))"); + } + + protected void checkBounds(String wkt) throws Exception { + String message = wkt.substring(0, wkt.indexOf('(')); + WKTReader reader = new WKTReader(factory); + + Geometry geometry = reader.read(wkt); + + MultiPolygon bounds = GeomUtils.toMultiPolygon(geometry); + checkBounds(message, geometry, bounds); + } + + protected void checkBounds(String message, Geometry geometry, MultiPolygon bounds){ + if(geometry ==null && bounds ==null) { + return; + } + assertNotNull(message+": geom", geometry ); + assertNotNull( message+": bounds", bounds ); + if( geometry instanceof GeometryCollection){ + GeometryCollection collection = (GeometryCollection) geometry; + for( int i=0; i < collection.getNumGeometries(); i++){ + assertTrue("coveredBy "+i, collection.getGeometryN(i).coveredBy(bounds)); + } + } + else { + assertTrue("coveredBy", geometry.coveredBy(bounds)); + } + assertEquals( "srid",geometry.getSRID(), bounds.getSRID() ); + assertEquals( "data", geometry.getUserData(), bounds.getUserData() ); + } +}