/*
* 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);
}
public ArcSdeSimplifyingFilterVisitor(SimpleFeatureType schema) {
super.setFeatureType(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) && featureType.getGeometryDescriptor() != null) {
name = featureType.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");
}
}
}
}