Search code examples
gisgeotools

How to utilize PropertyName in Geotools for a user-defined function?


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:

  1. The features are read correctly from the shapefile
  2. The function is actually called
  3. The Feature geometry is passed into the function
  4. Upon completion, the map is drawn

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;
}

Solution

  • 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")));