/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 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.data.transform;
import java.util.Arrays;
import java.util.List;
import org.geotools.factory.CommonFactoryFinder;
import org.geotools.feature.AttributeTypeBuilder;
import org.geotools.referencing.CRS;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.feature.type.AttributeDescriptor;
import org.opengis.filter.FilterFactory2;
import org.opengis.filter.expression.Expression;
import org.opengis.filter.expression.PropertyName;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import com.vividsolutions.jts.geom.Geometry;
/**
* Defines a transformed attribute to be used in {@link TransformFeatureSource}
*
* @author Andrea Aime - GeoSolutions
*/
public class Definition {
String name;
Expression expression;
Class binding;
CoordinateReferenceSystem crs;
static final FilterFactory2 FF = CommonFactoryFinder.getFilterFactory2();
/**
* Creates a new transformed property that mirrors 1-1 an existing property in the source type,
* without even renaming it
*
* @param name The property name
*/
public Definition(String name) {
this(name, null, null);
}
/**
* Creates a new transformed property
*
* @param name The property name
* @param source The expression generating the property
*/
public Definition(String name, Expression source) {
this(name, source, null);
}
/**
* Creates a new transformed property
*
* @param name The property name
* @param source The expression generating the property
* @param binding The property type. Optional, the store will try to figure out the type from
* the expression in case it's missing
*/
public Definition(String name, Expression source, Class binding) {
this(name, source, binding, null);
}
/**
* Creates a new transformed property
*
* @param name The property name
* @param source The expression generating the property
* @param binding The property type. Optional, the store will try to figure out the type from
* the expression in case it's missing
* @param crs The coordinate reference system of the property, to be used only for geometry
* properties
*/
public Definition(String name, Expression source, Class binding, CoordinateReferenceSystem crs) {
this.name = name;
if (source == null) {
this.expression = TransformFeatureSource.FF.property(name);
} else {
this.expression = source;
}
this.binding = binding;
this.crs = crs;
}
public String getName() {
return name;
}
public Expression getExpression() {
return expression;
}
public Class getBinding() {
return binding;
}
/**
* Returns the inverse to this Definition, that is, the definition of the source attribute
* corresponding to this computed attribute, if any. Only a small set of expression
* are invertible in general, and a smaller subset of that can be inverted by this method.
* Implementor can override this method to provide a custom inversion logic.
*
* @return The inverse of this definition, or null if not invertible or if the inversion
* logic for the specified case is missing
*/
public List<Definition> inverse() {
if(expression instanceof PropertyName) {
PropertyName pn = (PropertyName) expression;
return Arrays.asList(new Definition(pn.getPropertyName(), FF.property(name)));
}
// add a Point(x,y) function and then have it be split into x,y here (two definitions)
// TODO: look into algebraic inversion (e.g y = x + 3 -> x = y - 3) and into creating a concept
// of invertible function (which would work, with a bit of a loss in precision,
// for some math functions for example)
return null;
}
/**
* Computes the output attribute descriptor for this {@link Definition} given a sample feature
* of the original feature type. The code will first attempt a static analysis on the original
* feature type, if that fails it will try to evaluate the expression on the sample feature.
*
* @param originalFeature
* @return
*/
public AttributeDescriptor getAttributeDescriptor(SimpleFeature originalFeature) {
// try the static analysis
AttributeDescriptor ad = getAttributeDescriptor(originalFeature.getFeatureType());
if(ad != null) {
return ad;
}
// build it from the sample then
AttributeTypeBuilder ab = new AttributeTypeBuilder();
Object result = expression.evaluate(originalFeature);
Class computedBinding = null;
if (result != null) {
computedBinding = result.getClass();
}
CoordinateReferenceSystem computedCRS = crs;
if (Geometry.class.isAssignableFrom(computedBinding) && computedCRS == null) {
computedCRS = evaluateCRS(originalFeature);
}
ab.setBinding(computedBinding);
ab.setName(name);
if (computedCRS != null) {
ab.setCRS(computedCRS);
}
return ab.buildDescriptor(name);
}
/**
* Computes the output attribute descriptor for this {@link Definition} given only the original feature type.
* The code will attempt a static analysis on the original
* feature type, if that fails it will return null
*
* @param originalFeature
* @return
*/
public AttributeDescriptor getAttributeDescriptor(SimpleFeatureType originalSchema) {
AttributeTypeBuilder ab = new AttributeTypeBuilder();
ExpressionTypeEvaluator typeEvaluator = new ExpressionTypeEvaluator(originalSchema);
if (binding != null) {
ab.setBinding(binding);
ab.setName(name);
if (crs != null) {
ab.setCRS(crs);
} else {
// try to get it from the expression operands, under the assumption that
// the geometry crs are not getting modified by the filter functions
expression.accept(typeEvaluator, null);
ab.setCRS(typeEvaluator.getCoordinateReferenceSystem());
}
return ab.buildDescriptor(name);
} else {
// in case no evaluation succeeds
Class computedBinding = Object.class;
// see if we are just passing a property trough
if (expression instanceof PropertyName) {
PropertyName pn = (PropertyName) expression;
AttributeDescriptor descriptor = originalSchema.getDescriptor(pn.getPropertyName());
if (descriptor == null) {
throw new IllegalArgumentException(
"Original feature type does not have a property named " + name);
} else {
ab.init(descriptor);
ab.setName(name);
return ab.buildDescriptor(name);
}
} else {
// try static analysis
computedBinding = (Class) expression.accept(typeEvaluator, null);
if(computedBinding == null) {
return null;
}
CoordinateReferenceSystem computedCRS = crs;
if (Geometry.class.isAssignableFrom(computedBinding) && computedCRS == null) {
computedCRS = evaluateCRS(originalSchema);
}
ab.setBinding(computedBinding);
ab.setName(name);
if (computedCRS != null) {
ab.setCRS(computedCRS);
}
return ab.buildDescriptor(name);
}
}
}
private CoordinateReferenceSystem evaluateCRS(SimpleFeature originalFeature) {
SimpleFeatureType originalSchema = originalFeature.getFeatureType();
CoordinateReferenceSystem computedCRS = evaluateCRS(originalSchema);
if (computedCRS == null) {
// all right, let's try the sample feature then
Geometry g = expression.evaluate(originalFeature, Geometry.class);
if (g != null && g.getUserData() instanceof CoordinateReferenceSystem) {
computedCRS = (CoordinateReferenceSystem) g.getUserData();
} else {
try {
computedCRS = CRS.decode("EPSG:" + g.getSRID());
} catch (Exception e) {
return null;
}
}
}
return computedCRS;
}
private CoordinateReferenceSystem evaluateCRS(SimpleFeatureType originalSchema) {
return (CoordinateReferenceSystem) expression
.accept(new CRSEvaluator(originalSchema), null);
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((binding == null) ? 0 : binding.hashCode());
result = prime * result + ((crs == null) ? 0 : crs.hashCode());
result = prime * result + ((expression == null) ? 0 : expression.hashCode());
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Definition other = (Definition) obj;
if (binding == null) {
if (other.binding != null)
return false;
} else if (!binding.equals(other.binding))
return false;
if (crs == null) {
if (other.crs != null)
return false;
} else if (!crs.equals(other.crs))
return false;
if (expression == null) {
if (other.expression != null)
return false;
} else if (!expression.equals(other.expression))
return false;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
return true;
}
@Override
public String toString() {
return "Definition [name=" + name + ", binding=" + binding
+ ", crs=" + crs + ", expression=" + expression + "]";
}
}