/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2002-2008, 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.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.logging.Logger; import org.geotools.arcsde.data.ArcSDEGeometryBuilder; import org.geotools.factory.CommonFactoryFinder; import org.geotools.filter.FilterCapabilities; import org.geotools.filter.visitor.DefaultFilterVisitor; import org.opengis.feature.simple.SimpleFeatureType; import org.opengis.filter.And; import org.opengis.filter.Filter; import org.opengis.filter.FilterFactory2; import org.opengis.filter.FilterVisitor; import org.opengis.filter.Id; import org.opengis.filter.Not; import org.opengis.filter.Or; import org.opengis.filter.expression.Expression; import org.opengis.filter.expression.Literal; import org.opengis.filter.expression.PropertyName; import org.opengis.filter.spatial.BBOX; import org.opengis.filter.spatial.Beyond; import org.opengis.filter.spatial.BinarySpatialOperator; import org.opengis.filter.spatial.Contains; import org.opengis.filter.spatial.Crosses; import org.opengis.filter.spatial.DWithin; import org.opengis.filter.spatial.Disjoint; import org.opengis.filter.spatial.DistanceBufferOperator; import org.opengis.filter.spatial.Equals; import org.opengis.filter.spatial.Intersects; import org.opengis.filter.spatial.Overlaps; import org.opengis.filter.spatial.Touches; import org.opengis.filter.spatial.Within; import com.esri.sde.sdk.client.SeException; import com.esri.sde.sdk.client.SeExtent; import com.esri.sde.sdk.client.SeFilter; import com.esri.sde.sdk.client.SeLayer; import com.esri.sde.sdk.client.SeShape; import com.esri.sde.sdk.client.SeShapeFilter; import com.vividsolutions.jts.geom.Geometry; import com.vividsolutions.jts.geom.GeometryCollection; import com.vividsolutions.jts.geom.GeometryFactory; import com.vividsolutions.jts.geom.Polygon; /** * Encodes the geometry related parts of a filter into a set of <code>SeFilter</code> objects and * provides a method to get the resulting filters suitable to set up an SeQuery's spatial * constraints. * <p> * Although not all filters support is coded yet, the strategy to filtering queries for ArcSDE * datasources is separated in two parts, the SQL where clause construction, provided by * <code>FilterToSQLSDE</code> and the spatial filters (or spatial constraints, in SDE vocabulary) * provided here; mirroring the java SDE api approach * </p> * * @author Gabriel Rold?n * * * @source $URL$ * http://svn.geotools.org/geotools/trunk/gt/modules/plugin/arcsde/datastore/src/main/java * /org/geotools/arcsde/filter/GeometryEncoderSDE.java $ */ @SuppressWarnings("deprecation") public class GeometryEncoderSDE extends DefaultFilterVisitor implements FilterVisitor { /** Standard java logger */ private static Logger log = org.geotools.util.logging.Logging.getLogger("org.geotools.filter"); private static FilterCapabilities capabilities = new FilterCapabilities(); static { capabilities.addType(And.class); capabilities.addType(Not.class); // capabilities.addType(Or.class); capabilities.addType(Id.class); capabilities.addType(BBOX.class); capabilities.addType(Contains.class); capabilities.addType(Crosses.class); capabilities.addType(Disjoint.class); capabilities.addType(Equals.class); capabilities.addType(Intersects.class); capabilities.addType(Overlaps.class); capabilities.addType(Within.class); capabilities.addType(DWithin.class); capabilities.addType(Beyond.class); capabilities.addType(Touches.class); } private List<SeShapeFilter> sdeSpatialFilters; private SeLayer sdeLayer; private SimpleFeatureType featureType; /** */ public GeometryEncoderSDE() { // intentionally blank } /** */ public GeometryEncoderSDE(SeLayer layer, SimpleFeatureType featureType) { this.sdeLayer = layer; this.featureType = featureType; } public static FilterCapabilities getCapabilities() { return capabilities; } public SeFilter[] getSpatialFilters() { SeFilter[] filters = new SeFilter[this.sdeSpatialFilters.size()]; return (SeFilter[]) this.sdeSpatialFilters.toArray(filters); } private String getLayerName() throws SeException { if (this.sdeLayer == null) { throw new IllegalStateException("SDE layer has not been set"); } return this.sdeLayer.getQualifiedName(); } /** * overrides just to avoid the "WHERE" keyword */ public void encode(Filter filter) throws GeometryEncoderException { this.sdeSpatialFilters = new ArrayList<SeShapeFilter>(); if (Filter.INCLUDE.equals(filter)) { return; } if (capabilities.fullySupports(filter)) { filter.accept(this, null); } else { throw new GeometryEncoderException("Filter type " + filter.getClass() + " not supported"); } } /** * @param filter * @param sdeMethod * @param truth de default truth value for <code>sdeMethod</code> * @param extraData if an instanceof java.lang.Boolean, <code>truth</code> is and'ed with its * boolean value. May have been set by {@link #visit(Not, Object)} to revert the logical * evaluation criteria. */ private void addSpatialFilter(final BinarySpatialOperator filter, final int sdeMethod, final boolean truth, final Object extraData) { boolean appliedTruth = truth; // At the time of writing, extraData can only be null or false. // appliedTruth is calculated from following matrix. // // appliedTruth truth extraData // true ........false....false // false........true.....false // false........false....null // true.........true.....null if (extraData != null && extraData instanceof Boolean) { boolean andValue = ((Boolean) extraData).booleanValue(); if (andValue) { /** * TRUE ... should not occur, so fallback to old behaviour. */ appliedTruth = truth && andValue; } else { /** * FALSE ... toggle truth Parameter, so for example NOT DISJOINT works properly see * http://jira.codehaus.org/browse/GEOS-3735 for more information. */ appliedTruth = !truth; } } org.opengis.filter.expression.Expression left, right; PropertyName propertyExpr; Literal geomLiteralExpr; left = filter.getExpression1(); right = filter.getExpression2(); if (left instanceof PropertyName && right instanceof Literal) { propertyExpr = (PropertyName) left; geomLiteralExpr = (Literal) right; } else if (right instanceof PropertyName && left instanceof Literal) { propertyExpr = (PropertyName) right; geomLiteralExpr = (Literal) left; } else { String err = "SDE currently supports one geometry and one " + "attribute expr. You gave: " + left + ", " + right; throw new IllegalArgumentException(err); } // Should probably assert that attExpr's property name is equal to // spatialCol... // HACK: we want to support <namespace>:SHAPE, but current FM doesn't // support it. I guess we should try stripping the prefix and seeing if // that // matches... final String spatialCol = featureType.getGeometryDescriptor().getLocalName(); final String rawPropName = propertyExpr.getPropertyName(); String localPropName = rawPropName; if (rawPropName.indexOf(":") != -1) { localPropName = rawPropName.substring(rawPropName.indexOf(":") + 1); } if ("".equals(localPropName)) { log.fine("Empty property name found on filter, using default geometry property"); localPropName = spatialCol; } if (!rawPropName.equalsIgnoreCase(spatialCol) && !localPropName.equalsIgnoreCase(spatialCol)) { throw new IllegalArgumentException("When querying against a spatial " + "column, your property name must match the spatial" + " column name.You used '" + propertyExpr.getPropertyName() + "', but the DB's spatial column name is '" + spatialCol + "'"); } Geometry geom = (Geometry) geomLiteralExpr.getValue(); // To prevent errors in ArcSDE, we first trim the user's Filter // geometry to the extents of our layer. ArcSDEGeometryBuilder gb = ArcSDEGeometryBuilder.builderFor(Polygon.class); SeExtent seExtent = this.sdeLayer.getExtent(); // If a layer just has one point in it (or one very horizontal or // vertical line) then we may have // a layer extent that's a point or line. We need to correct this. if (seExtent.getMaxX() == seExtent.getMinX()) { seExtent = new SeExtent(seExtent.getMinX() - 100, seExtent.getMinY(), seExtent.getMaxX() + 100, seExtent.getMaxY()); } if (seExtent.getMaxY() == seExtent.getMinY()) { seExtent = new SeExtent(seExtent.getMinX(), seExtent.getMinY() - 100, seExtent.getMaxX(), seExtent.getMaxY() + 100); } try { // Now make an SeShape SeShape filterShape; if(seExtent.isEmpty() == true) { // The extent of the sdeLayer is uninitialised so create an extent. // If seExtent.isEmpty() == true, when passed to SeShape.generateRectangle() // an exception occurs. filterShape = new SeShape(this.sdeLayer.getCoordRef()); } else { SeShape extent = new SeShape(this.sdeLayer.getCoordRef()); extent.generateRectangle(seExtent); // this is a bit hacky, but I don't yet know this code well enough // to do it right. Basically if the geometry collection is // completely // outside of the area of the layer then an intersection will return // a geometryCollection (two seperate geometries not intersecting // will // be a collection of two). Passing this into GeometryBuilder causes // an exception. So what I did was just look to see if it is a gc // and if so then just make a null seshape, as it shouldn't match // any features in arcsde. -ch if (geom.getClass() == GeometryCollection.class) { filterShape = new SeShape(this.sdeLayer.getCoordRef()); } else { gb = ArcSDEGeometryBuilder.builderFor(geom.getClass()); filterShape = gb.constructShape(geom, this.sdeLayer.getCoordRef()); } } // Add the filter to our list SeShapeFilter shapeFilter = new SeShapeFilter(getLayerName(), this.sdeLayer.getSpatialColumn(), filterShape, sdeMethod, appliedTruth); this.sdeSpatialFilters.add(shapeFilter); } catch (IOException ioe) { throw new RuntimeException(ioe); } catch (SeException se) { throw new RuntimeException(se); } } // The Spatial Operator methods (these call to the above visit() method @Override public Object visit(BBOX filter, Object extraData) { addSpatialFilter(filter, SeFilter.METHOD_ENVP, true, extraData); return extraData; } @Override public Object visit(Contains filter, Object 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; } @Override public Object visit(Crosses filter, Object extraData) { addSpatialFilter(filter, SeFilter.METHOD_LCROSS_OR_CP, true, extraData); return extraData; } @Override public Object visit(Disjoint filter, Object extraData) { addSpatialFilter(filter, SeFilter.METHOD_II_OR_ET, false, extraData); return extraData; } @Override public Object visit(Touches filter, Object extraData) { // SFS definition of Touches says that two geometries 'a' and 'b' touch each other if the // intersection of the interiors of 'a' and 'b' is empty and if the intersection of 'a' and // 'b' is nonempty. // Hence we use a negated METHOD_II_NO_ET (Interior intersect and no edge touch search // method.) addSpatialFilter(filter, SeFilter.METHOD_II_NO_ET, false, extraData); return extraData; } @Override public Object visit(DWithin filter, Object extraData) { return visitDistanceBufferOperator(filter, true, extraData); } @Override public Object visit(Beyond filter, Object extraData) { return visitDistanceBufferOperator(filter, false, extraData); } /** * Converts a distance buffer op to an intersects againt the buffered input geometry */ private Object visitDistanceBufferOperator(DistanceBufferOperator filter, boolean truth, Object extraData) { // SDE can assert only one way, we need to invert from contains to within in case the // assertion is the other way around PropertyName property; Literal literal; { Expression expression1 = filter.getExpression1(); Expression expression2 = filter.getExpression2(); if (expression1 instanceof PropertyName && expression2 instanceof Literal) { property = (PropertyName) expression1; literal = (Literal) expression2; } else if (expression2 instanceof PropertyName && expression1 instanceof Literal) { property = (PropertyName) expression2; literal = (Literal) expression1; } else { // not supported throw new IllegalArgumentException("expected propertyname/literal, got " + expression1 + "/" + expression2); } } final Geometry geom = literal.evaluate(null, Geometry.class); final double distance = filter.getDistance(); final Geometry buffer = geom.buffer(distance); FilterFactory2 ff = CommonFactoryFinder.getFilterFactory2(); BinarySpatialOperator intersects = ff.intersects(property, ff.literal(buffer)); addSpatialFilter(intersects, SeFilter.METHOD_II_OR_ET, truth, extraData); return extraData; } @Override public Object visit(Equals filter, Object extraData) { addSpatialFilter(filter, SeFilter.METHOD_IDENTICAL, true, extraData); return extraData; } @Override public Object visit(Intersects filter, Object extraData) { addSpatialFilter(filter, SeFilter.METHOD_II_OR_ET, true, extraData); return extraData; } @Override public Object visit(Overlaps filter, Object extraData) { addSpatialFilter(filter, SeFilter.METHOD_II, true, 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; } @Override public Object visit(Within filter, Object 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; } @Override public Object visit(And filter, Object extraData) { List<Filter> children = filter.getChildren(); for (Filter child : children) { child.accept(this, extraData); } return extraData; } @Override public Object visit(Or filter, Object extraData) { List<Filter> children = filter.getChildren(); for (Filter child : children) { child.accept(this, extraData); } return extraData; } /** * Sets <code>extraData</code> to Boolean.FALSE to revert the truth value of the spatial filter * contained, if any. */ @Override public Object visit(Not filter, Object extraData) { Boolean truth = Boolean.FALSE; Filter negated = filter.getFilter(); return negated.accept(this, truth); } }