I am attempting to use the geotools.org library (Version 24) to write a user-defined function to style my GIS map according to an external data source. The documentation for the GeoTools library includes this tantalizing paragraph in the section on Functions:
When a function is used as part of a Style users often want to calculate a value based on the attributes of the Feature being drawn. The Expression PropertyName is used in this fashion to extract values out of a Feature and pass them into the function for evaluation.
This is exactly what I want to do. However, the documentation includes no actual example of how to do this.
I have spent several days trying various permutations of the Function definition, and I get the same result every time: My user-defined function only receives the geometry attribute, not the extra attributes I have specified.
I have verified that everything else works:
But I cannot get the Geotools library to pass in the additional Feature properties from the shapefile. Has anyone gotten this working, or can you even point me to an example of where this is used?
My current function definition:
package org.geotools.tutorial.function;
import mycode.data.dao.MyTableDao;
import mycode.data.model.MyTable;
import org.geotools.filter.FunctionExpressionImpl;
import org.geotools.filter.capability.FunctionNameImpl;
import org.opengis.feature.Feature;
import org.opengis.filter.capability.FunctionName;
import org.opengis.filter.expression.PropertyName;
import org.opengis.filter.expression.VolatileFunction;
import org.springframework.beans.factory.annotation.Autowired;
import java.awt.*;
import java.beans.Expression;
import java.util.Random;
public class DataFunction extends FunctionExpressionImpl {
@Autowired
MyTableDao myTableDao;
public static FunctionName NAME =
new FunctionNameImpl(
"DataFunction",
Color.class,
FunctionNameImpl.parameter("featureData1", PropertyName.class),
FunctionNameImpl.parameter("featureData2", PropertyName.class));
public DataFunction() {
super("DataFunction");
}
public int getArgCount() {
return 2;
}
@Override
public Object evaluate(Object feature) {
Feature f = (Feature) feature;
// fallback definition
float pct = 0.5F;
if (f.getProperty("featureData1") != null) {
MyTable temp = myTableDao.read(
f.getProperty("featureData1").getValue().toString(),
f.getProperty("featureData2").getValue().toString());
pct = temp.getColumnValue();
}
Color color = new Color(pct, pct, pct);
return color;
}
}
EDIT: I am invoking the function programmatically through a Style that is created in code. This Style is then added to the Layer which is added to the Map during the drawing process.
private Style createMagicStyle(File file, FeatureSource featureSource) {
FeatureType schema = (FeatureType) featureSource.getSchema();
// create a partially opaque outline stroke
Stroke stroke =
styleFactory.createStroke(
filterFactory.literal(Color.BLUE),
filterFactory.literal(1),
filterFactory.literal(0.5));
// create a partial opaque fill
Fill fill =
styleFactory.createFill(
filterFactory.function("DataFunction",
new AttributeExpressionImpl("featureData1"),
new AttributeExpressionImpl("featureData2")));
/*
* Setting the geometryPropertyName arg to null signals that we want to
* draw the default geomettry of features
*/
PolygonSymbolizer sym = styleFactory.createPolygonSymbolizer(stroke, fill, null);
Rule rule = styleFactory.createRule();
rule.symbolizers().add(sym);
FeatureTypeStyle fts = styleFactory.createFeatureTypeStyle(new Rule[] {rule});
Style style = styleFactory.createStyle();
style.featureTypeStyles().add(fts);
return style;
}
I think you want to set up your function to take a pair of Strings or Doubles (or whatever those attributes are) so something like:
public static FunctionName NAME =
new FunctionNameImpl(
"DataFunction",
Color.class,
FunctionNameImpl.parameter("featureData1", Double.class),
FunctionNameImpl.parameter("featureData2", Double.class));
public DataFunction(List<Expression> params, Literal fallback) {
this.params = params;
this.fallback = fallback;
}
then your evaluate
method becomes:
@Override
public Object evaluate(Object feature) {
Feature f = (Feature) feature;
// fallback definition
float pct = 0.5F;
Expression exp1 = params.get(0);
Expression exp2 = params.get(1);
if (f.getProperty("featureData1") != null) {
MyTable temp = myTableDao.read(
exp1.evaluate(f, Double.class),
exp2.evaluate(f, Double.class));
pct = temp.getColumnValue();
}
Color color = new Color(pct, pct, pct);
return color;
}
and you would use it in the style using something like:
Fill fill =
styleFactory.createFill(
filterFactory.function("DataFunction",
filterFactory.propertyName("featureData1"),
filterFactory.propertyName("featureData2")));