Author: aaime Date: 2012-02-29 07:44:21 -0800 (Wed, 29 Feb 2012) New Revision: 38589 Added: branches/2.7.x/modules/plugin/arcsde/datastore/src/main/java/org/geotools/arcsde/filter/ArcSdeSimplifyingFilterVisitor.java Modified: branches/2.7.x/modules/plugin/arcsde/datastore/src/main/java/org/geotools/arcsde/data/ArcSDEQuery.java branches/2.7.x/modules/plugin/arcsde/datastore/src/main/java/org/geotools/arcsde/filter/GeometryEncoderSDE.java branches/2.7.x/modules/plugin/arcsde/datastore/src/test/java/org/geotools/arcsde/data/FilterTest.java branches/2.7.x/modules/plugin/arcsde/datastore/src/test/java/org/geotools/arcsde/data/TestData.java Log: [GEOT-4051] ArcSDE store does not properly handle or-ed spatial filters Modified: branches/2.7.x/modules/plugin/arcsde/datastore/src/main/java/org/geotools/arcsde/data/ArcSDEQuery.java =================================================================== --- branches/2.7.x/modules/plugin/arcsde/datastore/src/main/java/org/geotools/arcsde/data/ArcSDEQuery.java 2012-02-29 15:37:37 UTC (rev 38588) +++ branches/2.7.x/modules/plugin/arcsde/datastore/src/main/java/org/geotools/arcsde/data/ArcSDEQuery.java 2012-02-29 15:44:21 UTC (rev 38589) @@ -33,6 +33,7 @@ import org.geotools.arcsde.ArcSdeException; import org.geotools.arcsde.data.FIDReader.SdeManagedFidReader; import org.geotools.arcsde.data.FIDReader.UserManagedFidReader; +import org.geotools.arcsde.filter.ArcSdeSimplifyingFilterVisitor; import org.geotools.arcsde.filter.FilterToSQLSDE; import org.geotools.arcsde.filter.GeometryEncoderException; import org.geotools.arcsde.filter.GeometryEncoderSDE; @@ -952,10 +953,15 @@ */ private void createGeotoolsFilters() { FilterToSQLSDE sqlEncoder = getSqlEncoder(); - + + // first off, simplify the filter + ArcSdeSimplifyingFilterVisitor visitor = new ArcSdeSimplifyingFilterVisitor(featureType); + Filter simplified = (Filter) sourceFilter.accept(visitor, null); + + // then perform the splits PostPreProcessFilterSplittingVisitor unpacker = new PostPreProcessFilterSplittingVisitor( sqlEncoder.getCapabilities(), featureType, null); - sourceFilter.accept(unpacker, null); + simplified.accept(unpacker, null); SimplifyingFilterVisitor filterSimplifier = new SimplifyingFilterVisitor(); final String typeName = this.featureType.getTypeName(); @@ -980,8 +986,9 @@ if (LOGGER.isLoggable(Level.FINE) && geometryFilter != null) LOGGER.fine("Spatial-Filter portion of SDE Query: '" + geometryFilter + "'"); - this.unsupportedFilter = unpacker.getFilterPost(); - this.unsupportedFilter = (Filter) this.unsupportedFilter.accept(filterSimplifier, null); + // SDE geometry filters are setup to be same or less restrictive than the JTS ones, + // so we do post filterin in memory with the full filter (it's fast anyways) + this.unsupportedFilter = remainingFilter; if (LOGGER.isLoggable(Level.FINE) && unsupportedFilter != null) LOGGER.fine("Unsupported (and therefore ignored) portion of SDE Query: '" + unsupportedFilter + "'"); Added: branches/2.7.x/modules/plugin/arcsde/datastore/src/main/java/org/geotools/arcsde/filter/ArcSdeSimplifyingFilterVisitor.java =================================================================== --- branches/2.7.x/modules/plugin/arcsde/datastore/src/main/java/org/geotools/arcsde/filter/ArcSdeSimplifyingFilterVisitor.java (rev 0) +++ branches/2.7.x/modules/plugin/arcsde/datastore/src/main/java/org/geotools/arcsde/filter/ArcSdeSimplifyingFilterVisitor.java 2012-02-29 15:44:21 UTC (rev 38589) @@ -0,0 +1,230 @@ +/* + * GeoTools - The Open Source Java GIS Toolkit + * http://geotools.org + * + * (C) 2002-2012, Open Source Geospatial Foundation (OSGeo) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License. + * + * This library 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 + * Lesser General Public License for more details. + * + */ +package org.geotools.arcsde.filter; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.geotools.factory.CommonFactoryFinder; +import org.geotools.filter.visitor.SimplifyingFilterVisitor; +import org.opengis.feature.simple.SimpleFeatureType; +import org.opengis.filter.Filter; +import org.opengis.filter.FilterFactory2; +import org.opengis.filter.Or; +import org.opengis.filter.expression.Literal; +import org.opengis.filter.expression.PropertyName; +import org.opengis.filter.spatial.BBOX; +import org.opengis.filter.spatial.BinarySpatialOperator; +import org.opengis.filter.spatial.Crosses; +import org.opengis.filter.spatial.Intersects; +import org.opengis.filter.spatial.Overlaps; +import org.opengis.filter.spatial.Touches; + +import com.vividsolutions.jts.geom.Geometry; +import com.vividsolutions.jts.geom.GeometryCollection; + +/** + * Custom simplifying filter visitor that also tries to turn multiple or-ed + * spatial filters into a single spatial filter (since ArcSDE query cannot + * handle or-ed spatial conditions) + * + * @author Andrea Aime - GeoSolutions + */ +public class ArcSdeSimplifyingFilterVisitor extends SimplifyingFilterVisitor { + static final FilterFactory2 FF = CommonFactoryFinder.getFilterFactory2(null); + + // this is the list of spatial operations that we can merge upon (that is, it's not the full list) + static final Set<Class<? extends BinarySpatialOperator>> SPATIAL_OPERATIONS = new HashSet<Class<? extends BinarySpatialOperator>>(); + static { + SPATIAL_OPERATIONS.add(BBOX.class); + SPATIAL_OPERATIONS.add(Intersects.class); + SPATIAL_OPERATIONS.add(Crosses.class); + SPATIAL_OPERATIONS.add(Overlaps.class); + // commented out for the moment, we could merge but only if the distance is the same + // SPATIAL_OPERATIONS.add(DWithin.class); + // commented out for the moment, we could merge but only if they are not two overlapping + // polygons, as the union will remove some borders on that case + // SPATIAL_OPERATIONS.add(Touches.class); + } + + private SimpleFeatureType schema; + + public ArcSdeSimplifyingFilterVisitor(SimpleFeatureType schema) { + this.schema = schema; + } + + @Override + public Object visit(Or filter, Object extraData) { + // perform the standard simplification + Filter simplified = (Filter) super.visit(filter, extraData); + + // is it still an Or filter? + if(simplified instanceof Or) { + // collect spatial filters so that they are separated per attribute + Map<String, List<SpatialOperation>> spatialOps = new HashMap<String, List<SpatialOperation>>(); + List<Filter> otherFilters = new ArrayList<Filter>(); + List<Filter> children = ((Or) simplified).getChildren(); + for (Filter child : children) { + // we know how to merge only bbox and intersects for the moment + if(child instanceof BinarySpatialOperator) { + BinarySpatialOperator bso = (BinarySpatialOperator) child; + String name = null; + SpatialOperation so = null; + if(bso.getExpression1() instanceof PropertyName && bso.getExpression2() instanceof Literal) { + name = ((PropertyName) bso.getExpression1()).getPropertyName(); + so = new SpatialOperation(bso); + } else if(bso.getExpression2() instanceof PropertyName && bso.getExpression1() instanceof Literal) { + name = ((PropertyName) bso.getExpression2()).getPropertyName(); + so = new SpatialOperation(bso); + } + + if(name != null && so != null) { + // handle the default geometry case + if("".equals(name) && schema.getGeometryDescriptor() != null) { + name = schema.getGeometryDescriptor().getLocalName(); + } + + // collect into the specific geometry list + List<SpatialOperation> list = spatialOps.get(name); + if(list == null) { + list = new ArrayList<ArcSdeSimplifyingFilterVisitor.SpatialOperation>(); + spatialOps.put(name, list); + } + list.add(so); + } else { + // cannot handle this one + otherFilters.add(child); + } + } else { + otherFilters.add(child); + } + } + + // try to merge all filters that work agains the same attribute and perform the same + // (or similar enough) operation + List<Filter> mergedFilters = new ArrayList<Filter>(); + for (String property : spatialOps.keySet()) { + List<SpatialOperation> propertyFilters = spatialOps.get(property); + + // we perform a reduction on the list of filters, trying to find groups that can be merged + while(propertyFilters.size() > 0) { + SpatialOperation main = propertyFilters.get(0); + List<SpatialOperation> toMerge = new ArrayList<SpatialOperation>(); + toMerge.add(main); + for (int j = 1; j < propertyFilters.size(); ) { + SpatialOperation secondary = propertyFilters.get(j); + // check if the two operations are compatible + if(secondary.operation == main.operation + || (secondary.operation == BBOX.class && main.operation == Intersects.class) + || (secondary.operation == Intersects.class && main.operation == BBOX.class)) { + toMerge.add(secondary); + propertyFilters.remove(j); + } else { + j++; + } + } + + if(toMerge.size() == 1) { + // could not be merged, put in the "others" list + otherFilters.add(main.op); + } else { + try { + Filter merged = mergeOperations(property, toMerge); + mergedFilters.add(merged); + } catch(Exception e) { + // the operation can go belly up because of topology exceptions, in + // that case we just add back all the operations to the main list + for (SpatialOperation so : toMerge) { + otherFilters.add(so.op); + } + } + } + propertyFilters.remove(0); + } + } + + // did we manage to squash anything? + if(mergedFilters.size() == 1 && otherFilters.size() == 0) { + simplified = mergedFilters.get(0); + } else if(mergedFilters.size() > 0) { + List<Filter> full = new ArrayList<Filter>(); + full.addAll(mergedFilters); + full.addAll(otherFilters); + simplified = FF.or(full); + } + } + + return simplified; + } + + private Filter mergeOperations(String propertyName, List<SpatialOperation> ops) { + // prepare the property name + PropertyName property = FF.property(propertyName); + + // prepare united the geometry + Geometry[] geomArray = new Geometry[ops.size()]; + for (int i = 0; i < geomArray.length; i++) { + geomArray[i] = ops.get(i).geometry; + } + GeometryCollection collection = geomArray[0].getFactory().createGeometryCollection(geomArray); + Geometry united = collection.union(); + Literal geometry = FF.literal(united); + + // rebuild the filter + Class operation = ops.get(0).operation; + if(BBOX.class.isAssignableFrom(operation) || Intersects.class.isAssignableFrom(operation)) { + return FF.intersects(property, geometry); + } else if(Crosses.class.isAssignableFrom(operation)) { + return FF.crosses(property, geometry); + } else if(Overlaps.class.isAssignableFrom(operation)) { + return FF.overlaps(property, geometry); + } else if(Touches.class.isAssignableFrom(operation)) { + return FF.touches(property, geometry); + } else { + throw new IllegalArgumentException("Cannot merge operation " + operation.getName()); + } + } + + + static class SpatialOperation { + BinarySpatialOperator op; + Class operation; + Geometry geometry; + + public SpatialOperation(BinarySpatialOperator op) { + this.op = op; + for (Class iface : op.getClass().getInterfaces()) { + if(SPATIAL_OPERATIONS.contains(iface)) { + operation = iface; + break; + } + } + if(op.getExpression1() instanceof Literal) { + geometry = op.getExpression1().evaluate(null, Geometry.class); + } else if(op.getExpression2() instanceof Literal) { + geometry = op.getExpression2().evaluate(null, Geometry.class); + } else { + throw new IllegalArgumentException("Cannot find literal geometry in the spatial filter"); + } + } + } +} Modified: branches/2.7.x/modules/plugin/arcsde/datastore/src/main/java/org/geotools/arcsde/filter/GeometryEncoderSDE.java =================================================================== --- branches/2.7.x/modules/plugin/arcsde/datastore/src/main/java/org/geotools/arcsde/filter/GeometryEncoderSDE.java 2012-02-29 15:37:37 UTC (rev 38588) +++ branches/2.7.x/modules/plugin/arcsde/datastore/src/main/java/org/geotools/arcsde/filter/GeometryEncoderSDE.java 2012-02-29 15:44:21 UTC (rev 38589) @@ -92,7 +92,10 @@ private static FilterCapabilities capabilities = new FilterCapabilities(); static { - capabilities.addAll(FilterCapabilities.LOGICAL_OPENGIS); + capabilities.addType(And.class); + capabilities.addType(Not.class); + // capabilities.addType(Or.class); + capabilities.addType(Id.class); capabilities.addType(BBOX.class); @@ -307,7 +310,13 @@ } public Object visit(Contains filter, Object extraData) { - addSpatialFilter(filter, SeFilter.METHOD_PC, true, extraData); + // SDE can assert only one way, we need to invert from contains to within in case the + // assertion is the other way around + if (filter.getExpression1() instanceof PropertyName && filter.getExpression2() instanceof Literal) { + addSpatialFilter(filter, SeFilter.METHOD_PC, true, extraData); + } else { + addSpatialFilter(filter, SeFilter.METHOD_SC, true, extraData); + } return extraData; } @@ -341,13 +350,23 @@ public Object visit(Overlaps filter, Object extraData) { addSpatialFilter(filter, SeFilter.METHOD_II, true, extraData); - addSpatialFilter(filter, SeFilter.METHOD_PC, false, extraData); - addSpatialFilter(filter, SeFilter.METHOD_SC, false, extraData); + // AA: nope, Overlaps definition is The geometries have some but not all points in common, + // they have the same dimension, and the intersection of the interiors of the two geometries + // has the same dimension as the geometries themselves. + // --> that is, one can be contained in the other and they still overlap + // addSpatialFilter(filter, SeFilter.METHOD_PC, false, extraData); + // addSpatialFilter(filter, SeFilter.METHOD_SC, false, extraData); return extraData; } public Object visit(Within filter, Object extraData) { - addSpatialFilter(filter, SeFilter.METHOD_SC, true, extraData); + // SDE can assert only one way, we need to invert from contains to within in case the + // assertion is the other way around + if (filter.getExpression1() instanceof PropertyName && filter.getExpression2() instanceof Literal) { + addSpatialFilter(filter, SeFilter.METHOD_SC, true, extraData); + } else { + addSpatialFilter(filter, SeFilter.METHOD_PC, true, extraData); + } return extraData; } Modified: branches/2.7.x/modules/plugin/arcsde/datastore/src/test/java/org/geotools/arcsde/data/FilterTest.java =================================================================== --- branches/2.7.x/modules/plugin/arcsde/datastore/src/test/java/org/geotools/arcsde/data/FilterTest.java 2012-02-29 15:37:37 UTC (rev 38588) +++ branches/2.7.x/modules/plugin/arcsde/datastore/src/test/java/org/geotools/arcsde/data/FilterTest.java 2012-02-29 15:44:21 UTC (rev 38589) @@ -17,8 +17,7 @@ */ package org.geotools.arcsde.data; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.*; import java.io.IOException; import java.util.ArrayList; @@ -49,6 +48,7 @@ import org.opengis.feature.type.PropertyDescriptor; import org.opengis.filter.Filter; import org.opengis.filter.FilterFactory2; +import org.opengis.filter.spatial.BBOX; import com.vividsolutions.jts.geom.Coordinate; import com.vividsolutions.jts.geom.Geometry; @@ -196,37 +196,49 @@ * TODO: resurrect testDisjointFilter */ @Test - @Ignore public void testDisjointFilter() throws Exception { FeatureType ft = this.dataStore.getSchema(testData.getTempTableName()); - // Build the filter - double minx = -179; - double maxx = -170; - double miny = -90; - double maxy = -80; - Polygon p = buildPolygon(minx, miny, maxx, maxy); + // Build a polygon that intercepts some of the geometries, but not all of them + Polygon p = buildPolygon(-180, 0, -160, 90); Filter filter = ff.not(ff.isNull(ff.property("SHAPE"))); filter = ff.and(filter, ff.disjoint(ff.property("SHAPE"), ff.literal(p))); - runTestWithFilter(ft, filter); + runTestWithFilter(ft, filter, false); } @Test public void testContainsFilter() throws Exception { FeatureType ft = this.dataStore.getSchema(testData.getTempTableName()); - // Build the filter - double minx = 106.6666; - double maxx = 106.6677; - double miny = -6.1676; - double maxy = -6.1672; - Polygon p = buildPolygon(minx, miny, maxx, maxy); + // Build the filter with a polygon that is inside POLYGON((-10 -10, -10 10, 10 10, 10 -10, -10 -10)) + Polygon p = buildPolygon(-9, -9, -8, -8); + Filter filter = ff.contains(ff.property("SHAPE"), ff.literal(p)); + runTestWithFilter(ft, filter, false); + + // now build the opposite filter, the polygon contains the shape + p = buildPolygon(-1, -1, 1, 1); + filter = ff.contains(ff.literal(p), ff.property("SHAPE")); + runTestWithFilter(ft, filter, false); + } + + @Test + public void testContainsSDESemanticsFilter() throws Exception { + FeatureType ft = this.dataStore.getSchema(testData.getTempTableName()); + // Build a filter so that SDE would actually catch more geometries, it would + // actually include "MULTIPOLYGON( ((-1 -1, -1 1, 1 1, 1 -1, -1 -1)), ((-170 -80, -170 -70, -160 -70, -160 -80, -170 -80)) )" + // in the results as well. It seems the containment semantics is applied in or to the multigeometry + // components. We do in memory post filtering to get the right semantics + Polygon p = buildPolygon(-1, -1, 1, 1); Filter filter = ff.contains(ff.property("SHAPE"), ff.literal(p)); - - runTestWithFilter(ft, filter); + runTestWithFilter(ft, filter, false); + + // now build the opposite filter, the polygon contains the shape + p = buildPolygon(-1, -1, 1, 1); + filter = ff.contains(ff.literal(p), ff.property("SHAPE")); + runTestWithFilter(ft, filter, false); } @Test @@ -234,28 +246,36 @@ FeatureType ft = this.dataStore.getSchema(testData.getTempTableName()); // Build the filter - double minx = 106.6337; - double maxx = 106.6381; - double miny = -6.1794; - double maxy = -6.1727; - Filter filter = ff.bbox("SHAPE", minx, miny, maxx, maxy, "EPSG:4326"); + Filter filter = ff.bbox("SHAPE", -1, -1, 1, 1, "EPSG:4326"); - runTestWithFilter(ft, filter); + runTestWithFilter(ft, filter, false); } + + @Test + public void testOrBBoxFilter() throws Exception { + FeatureType ft = this.dataStore.getSchema(testData.getTempTableName()); + + System.out.println(this.dataStore.getFeatureSource(ft.getName().getLocalPart()).getBounds()); + // build a or of bbox so that + // - the intersection of the bboxes is empty + // - the union of the bboxes actually gets more data than necessary + BBOX bbox1 = ff.bbox("SHAPE", -171, -90, -169, 90, "EPSG:4326"); + BBOX bbox2 = ff.bbox("SHAPE", 169, -90, 171, 90, "EPSG:4326"); + Filter filter = ff.or(bbox1, bbox2); + + runTestWithFilter(ft, filter, false); + } + @Test public void testIntersectsFilter() throws Exception { FeatureType ft = this.dataStore.getSchema(testData.getTempTableName()); // Build the filter - double minx = 106.6337; - double maxx = 106.6381; - double miny = -6.1794; - double maxy = -6.1727; - Polygon p = buildPolygon(minx, miny, maxx, maxy); + Polygon p = buildPolygon(-1, -1, 1, 1); Filter filter = ff.intersects(ff.property("SHAPE"), ff.literal(p)); - runTestWithFilter(ft, filter); + runTestWithFilter(ft, filter, false); } @Test @@ -263,14 +283,10 @@ FeatureType ft = this.dataStore.getSchema(testData.getTempTableName()); // Build the filter - double minx = 106.6337; - double maxx = 106.6381; - double miny = -6.1794; - double maxy = -6.1727; - Polygon p = buildPolygon(minx, miny, maxx, maxy); + Polygon p = buildPolygon(-10, -10, -8, -8); Filter filter = ff.overlaps(ff.property("SHAPE"), ff.literal(p)); - runTestWithFilter(ft, filter); + runTestWithFilter(ft, filter, false); } @Test @@ -278,14 +294,16 @@ FeatureType ft = this.dataStore.getSchema(testData.getTempTableName()); // Build the filter - double minx = 106.6337; - double maxx = 106.6381; - double miny = -6.1794; - double maxy = -6.1727; - Polygon p = buildPolygon(minx, miny, maxx, maxy); - Filter filter = ff.within(ff.property("SHAPE"), ff.literal(p)); + Polygon p = buildPolygon(-9, -9, -8, -8); + Filter filter = ff.within(ff.literal(p), ff.property("SHAPE")); + runTestWithFilter(ft, filter, false); + + // now build the opposite filter, the polygon contains the shape + p = buildPolygon(-1, -1, 1, 1); + filter = ff.within(ff.property("SHAPE"), ff.literal(p)); + runTestWithFilter(ft, filter, false); - runTestWithFilter(ft, filter); + runTestWithFilter(ft, filter, false); } @Test @@ -293,14 +311,10 @@ FeatureType ft = this.dataStore.getSchema(testData.getTempTableName()); // Build the filter - double minx = 106.6337; - double maxx = 106.6381; - double miny = -6.1794; - double maxy = -6.1727; - LineString ls = buildSegment(minx, miny, maxx, maxy); + LineString ls = buildSegment(-12, -12, 12, 12); Filter filter = ff.crosses(ff.property("SHAPE"), ff.literal(ls)); - runTestWithFilter(ft, filter); + runTestWithFilter(ft, filter, false); } /** @@ -325,10 +339,10 @@ // Build the filter Filter filter = ff.equal(ff.property("SHAPE"), ff.literal(g)); - runTestWithFilter(ft, filter); + runTestWithFilter(ft, filter, false); } - private void runTestWithFilter(FeatureType ft, Filter filter) throws Exception { + private void runTestWithFilter(FeatureType ft, Filter filter, boolean empty) throws Exception { System.err.println("****************"); System.err.println("**"); System.err.println("** TESTING FILTER: " + filter); @@ -364,8 +378,15 @@ fr.close(); endTime = System.currentTimeMillis(); System.err.println("Fast read took " + (endTime - startTime) + " milliseconds."); + + assertFeatureListsSimilar(slowResults, fastResults); + + if(empty) { + assertEquals("Result was supposed to be empty", 0, fastResults.size()); + } else { + assertTrue("Result was supposed to be non empty", fastResults.size() > 0); + } - assertFeatureListsSimilar(slowResults, fastResults); } private String[] safePropertyNames(FeatureType ft) { Modified: branches/2.7.x/modules/plugin/arcsde/datastore/src/test/java/org/geotools/arcsde/data/TestData.java =================================================================== --- branches/2.7.x/modules/plugin/arcsde/datastore/src/test/java/org/geotools/arcsde/data/TestData.java 2012-02-29 15:37:37 UTC (rev 38588) +++ branches/2.7.x/modules/plugin/arcsde/datastore/src/test/java/org/geotools/arcsde/data/TestData.java 2012-02-29 15:44:21 UTC (rev 38589) @@ -556,13 +556,13 @@ * <li>7. Shape - values: * <ul> * <li> <code>POINT(0 0)</code> - * <li> <code>MULTIPOINT(0 0, 180 0)</code> - * <li> <code>LINESTRING(0 0, 180 90)</code> - * <li> <code>MULTILINESTRING((-180 -90, 180 90), (-180 90, 180 -90))</code> + * <li> <code>MULTIPOINT(0 0, 170 0)</code> + * <li> <code>LINESTRING(0 0, 170 80)</code> + * <li> <code>"MULTILINESTRING((-170 -80, 170 80), (-170 80, 170 -80))"</code> * <li> <code>POLYGON((-10 -10, -10 10, 10 10, 10 -10, -10 -10))</code> * <li> - * <code>MULTIPOLYGON( ((-1 -1, -1 1, 1 1, 1 -1, -1 -1)), ((-180 -90, -180 -80, -170 -80, -170 -90, -180 -90)) )</code> - * <li> <code>GEOMETRYCOLLECTION(POINT(1 1), LINESTRING(0 0, 180 90))</code> + * <code>MULTIPOLYGON( "MULTIPOLYGON( ((-1 -1, -1 1, 1 1, 1 -1, -1 -1)), ((-170 -80, -170 -70, -160 -70, -160 -80, -170 -80)) )" )</code> + * <li> <code>POINT EMPTY</code> * <li> <code>null</code> * </ul> * </li> |