/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2014-2015, 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.filter.visitor;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.geotools.filter.FilterAttributeExtractor;
import org.geotools.util.Range;
import org.opengis.feature.type.FeatureType;
import org.opengis.filter.BinaryComparisonOperator;
import org.opengis.filter.BinaryLogicOperator;
import org.opengis.filter.Filter;
import org.opengis.filter.FilterFactory2;
import org.opengis.filter.PropertyIsBetween;
import org.opengis.filter.PropertyIsEqualTo;
import org.opengis.filter.PropertyIsGreaterThan;
import org.opengis.filter.PropertyIsGreaterThanOrEqualTo;
import org.opengis.filter.PropertyIsLessThan;
import org.opengis.filter.PropertyIsLessThanOrEqualTo;
import org.opengis.filter.PropertyIsNotEqualTo;
import org.opengis.filter.expression.Expression;
import org.opengis.filter.expression.Literal;
import org.opengis.filter.expression.PropertyName;
/**
* Utility class used by {@link SimplifyingFilterVisitor} to combine range based filters. This class
* works correctly only if all the range based filters have the same MatchAction, but does not check
* it internally, so it is suitable for usage only in a simple feature context
*
* @author Andrea Aime - GeoSolutions
*
*/
abstract class RangeCombiner {
/**
* Combines ranges by unioning them when possible
*
* @author Andrea Aime - GeoSolutions
*
*/
static class Or extends RangeCombiner {
public Or(FilterFactory2 ff, FeatureType featureType, List<Filter> filters) {
super(ff, featureType, filters);
}
@Override
protected MultiRange combineRanges(MultiRange r1, MultiRange r2) {
return r1.merge(r2);
}
@Override
protected void addFiltersToResults(List<Filter> results, Filter filter) {
if (filter instanceof org.opengis.filter.Or) {
results.addAll(((org.opengis.filter.Or) filter).getChildren());
} else {
results.add(filter);
}
}
}
/**
* Combines ranges by intersecting them
*
* @author Andrea Aime - GeoSolutions
*
*/
static class And extends RangeCombiner {
public And(FilterFactory2 ff, FeatureType featureType, List<Filter> filters) {
super(ff, featureType, filters);
}
@Override
protected MultiRange combineRanges(MultiRange r1, MultiRange r2) {
return r1.intersect(r2);
}
@Override
protected void addFiltersToResults(List<Filter> results, Filter filter) {
if (filter instanceof org.opengis.filter.And) {
results.addAll(((org.opengis.filter.And) filter).getChildren());
} else {
results.add(filter);
}
}
}
/**
* A tuple made of a filter and its associated range
*
* @author Andrea Aime - GeoSolutions
*/
static class FilterRange {
Filter filter;
Range range;
public FilterRange(Filter filter, Range range) {
this.filter = filter;
this.range = range;
}
}
/**
* A tuple made of an expression and its associated range
*
* @author Andrea Aime - GeoSolutions
*/
static class ExpressionRange {
Expression expression;
Range range;
public ExpressionRange(Expression expression, Range range) {
this.expression = expression;
this.range = range;
}
}
static class CombinationResult {
Map<Expression, List<FilterRange>> rangeMap;
boolean combinationHappened;
public CombinationResult(Map<Expression, List<FilterRange>> rangeMap,
boolean combinationHappened) {
this.rangeMap = rangeMap;
this.combinationHappened = combinationHappened;
}
}
ExpressionTypeVisitor expressionTypeVisitor;
Map<Expression, MultiRange> rangeMap = new HashMap<>();
FeatureType featureType;
List<Filter> otherFilters = new ArrayList<Filter>();
List<Filter> filters;
FilterFactory2 ff;
public RangeCombiner(FilterFactory2 ff, FeatureType featureType, List<Filter> filters) {
this.ff = ff;
this.filters = filters;
this.featureType = featureType;
this.expressionTypeVisitor = new ExpressionTypeVisitor(featureType);
// now organize by comparison filters to apply range based simplification
for (Filter f : filters) {
if (f instanceof PropertyIsBetween) {
PropertyIsBetween pb = (PropertyIsBetween) f;
Class binding = getTypeIfComparable(pb.getExpression());
if (binding == null) {
otherFilters.add(pb);
} else {
Object min = evaluate(pb.getLowerBoundary(), binding);
Object max = evaluate(pb.getUpperBoundary(), binding);
if (min == null || max == null) {
otherFilters.add(f);
} else {
Expression expression = pb.getExpression();
Range<?> range = new Range(binding, (Comparable) min, (Comparable) max);
addRange(rangeMap, expression, new MultiRange(range));
}
}
} else if (f instanceof PropertyIsNotEqualTo) {
PropertyIsNotEqualTo ne = (PropertyIsNotEqualTo) f;
Comparable exclusion = null;
Expression expression = null;
Class<Comparable<?>> binding = getTypeIfComparable(ne.getExpression1());
if (binding != null) {
expression = ne.getExpression1();
if (binding != null) {
exclusion = evaluate(ne.getExpression2(), binding);
}
} else {
expression = ne.getExpression2();
binding = getTypeIfComparable(ne.getExpression2());
if (binding != null) {
exclusion = evaluate(ne.getExpression1(), binding);
}
}
if (exclusion != null) {
addRange(rangeMap, expression, new MultiRange(binding, exclusion));
} else {
otherFilters.add(f);
}
} else if (f instanceof BinaryComparisonOperator) {
BinaryComparisonOperator op = (BinaryComparisonOperator) f;
ExpressionRange er = getRange(op);
// Right now, only PropertyIsEqualTo actually considers matchcase, all others
// behave as if they were case sensitive regardgless of the setting.
// TODO: change the logic to consider matchcase when
if (er.range != null
&& (!(op instanceof PropertyIsEqualTo) || (op.isMatchingCase()))) {
addRange(rangeMap, er.expression, new MultiRange(er.range));
} else {
otherFilters.add(f);
}
} else if (f instanceof org.opengis.filter.And || f instanceof org.opengis.filter.Or) {
BinaryLogicOperator logic = (BinaryLogicOperator) f;
List<Filter> children = logic.getChildren();
RangeCombiner subCombiner;
if (logic instanceof org.opengis.filter.And) {
subCombiner = new RangeCombiner.And(ff, featureType, children);
} else {
subCombiner = new RangeCombiner.Or(ff, featureType, children);
}
// see if the sub-filter can be assimilated to a single range
if (!subCombiner.otherFilters.isEmpty()
|| (subCombiner.rangeMap.size() > 1 && !subCombiner.getClass().equals(
this.getClass()))) {
otherFilters.add(f);
} else {
Map<Expression, MultiRange> combined = subCombiner.rangeMap;
for (Map.Entry<Expression, MultiRange> entry : combined.entrySet()) {
Expression ex = entry.getKey();
MultiRange ranges = entry.getValue();
addRange(rangeMap, ex, ranges);
}
}
} else {
// negation of ranges is not handled by choice, as the simplifying filter visitor has already
// switched negations out for us
otherFilters.add(f);
}
}
}
private ExpressionRange getRange(BinaryComparisonOperator op) {
Range range = null;
Expression expression = null;
if (!(isStatic(op.getExpression1()))) {
expression = op.getExpression1();
Class binding = getTypeIfComparable(expression);
if (binding != null) {
Object value = evaluate(op.getExpression2(), binding);
if (value != null) {
if (op instanceof PropertyIsLessThan) {
range = new Range(binding, null, false, (Comparable) value, false);
} else if (op instanceof PropertyIsLessThanOrEqualTo) {
range = new Range(binding, null, false, (Comparable) value, true);
} else if (op instanceof PropertyIsEqualTo) {
range = new Range(binding, (Comparable) value, (Comparable) value);
} else if (op instanceof PropertyIsGreaterThanOrEqualTo) {
range = new Range(binding, (Comparable) value, true, null, false);
} else if (op instanceof PropertyIsGreaterThan) {
range = new Range(binding, (Comparable) value, false, null, false);
}
}
}
} else if (!isStatic(op.getExpression2())) {
expression = op.getExpression2();
Class binding = getTypeIfComparable(expression);
if (binding != null) {
Object value = evaluate(op.getExpression1(), binding);
if (value != null) {
if (op instanceof PropertyIsLessThan) {
range = new Range(binding, (Comparable) value, true, null, false);
} else if (op instanceof PropertyIsLessThanOrEqualTo) {
range = new Range(binding, (Comparable) value, false, null, false);
} else if (op instanceof PropertyIsEqualTo) {
range = new Range(binding, (Comparable) value, (Comparable) value);
} else if (op instanceof PropertyIsGreaterThanOrEqualTo) {
range = new Range(binding, null, false, (Comparable) value, false);
} else if (op instanceof PropertyIsGreaterThan) {
range = new Range(binding, null, false, (Comparable) value, true);
}
}
}
}
return new ExpressionRange(expression, range);
}
private boolean isStatic(Expression exp) {
FilterAttributeExtractor attributeExtractor = new FilterAttributeExtractor();
exp.accept(attributeExtractor, null);
return attributeExtractor.getAttributeNameSet().isEmpty();
}
private Class getTypeIfComparable(Expression ex) {
Class type = (Class) ex.accept(expressionTypeVisitor, null);
if (Comparable.class.isAssignableFrom(type)) {
return type;
} else {
return null;
}
}
String getPropertyName(Expression ex) {
if (ex instanceof PropertyName) {
PropertyName pn = (PropertyName) ex;
return pn.getPropertyName();
}
return null;
}
public List<Filter> getReducedFilters() {
if (rangeMap.isEmpty()) {
return filters;
}
List<Filter> result = new ArrayList<>(otherFilters);
for (Expression ex : new ArrayList<Expression>(rangeMap.keySet())) {
MultiRange multiRange = rangeMap.get(ex);
addFiltersToResults(result, multiRange.toFilter(ff, ex));
}
return result;
}
protected abstract void addFiltersToResults(List<Filter> result, Filter filter);
/**
* Combines two multiranges
*/
protected abstract MultiRange combineRanges(MultiRange r1, MultiRange r2);
private void addRange(Map<Expression, MultiRange> rangeMap, Expression expression,
MultiRange other) {
MultiRange ranges = rangeMap.get(expression);
if (ranges == null) {
rangeMap.put(expression, other);
} else {
MultiRange combined = combineRanges(ranges, other);
rangeMap.put(expression, combined);
}
}
private <T> T evaluate(Expression ex, Class<T> target) {
return ex instanceof Literal ? ex.evaluate(null, target) : null;
}
}