Function Tutorial

Adding a Function to GeoTools a very useful introduction to extending the library. This is often used to generate values when styling that you cannot otherwise accomplish using expressions.

SnapFunction

  1. Here is a quick implementation of snapping a point to a line:

    /*
     *    GeoTools Sample code and Tutorials by Open Source Geospatial Foundation, and others
     *    https://docs.geotools.org
     *
     *    To the extent possible under law, the author(s) have dedicated all copyright
     *    and related and neighboring rights to this software to the public domain worldwide.
     *    This software is distributed without any warranty.
     *
     *    You should have received a copy of the CC0 Public Domain Dedication along with this
     *    software. If not, see <http://creativecommons.org/publicdomain/zero/1.0/>.
     */
    package org.geotools.tutorial.function;
    
    import java.util.List;
    import org.geotools.api.filter.capability.FunctionName;
    import org.geotools.api.filter.expression.Expression;
    import org.geotools.api.filter.expression.ExpressionVisitor;
    import org.geotools.api.filter.expression.Function;
    import org.geotools.api.filter.expression.Literal;
    import org.geotools.filter.capability.FunctionNameImpl;
    import org.geotools.util.Converters;
    import org.locationtech.jts.geom.Coordinate;
    import org.locationtech.jts.geom.Geometry;
    import org.locationtech.jts.geom.Point;
    import org.locationtech.jts.linearref.LinearLocation;
    import org.locationtech.jts.linearref.LocationIndexedLine;
    
    /**
     * Quick function that illustrates snapping to a line.
     *
     * @author jody
     */
    public class SnapFunction implements Function {
    
        static FunctionName NAME =
                new FunctionNameImpl(
                        "snap",
                        Point.class,
                        FunctionNameImpl.parameter("point", Point.class),
                        FunctionNameImpl.parameter("line", Geometry.class));
    
        private final List<Expression> parameters;
    
        private final Literal fallback;
    
        public SnapFunction(List<Expression> parameters, Literal fallback) {
            if (parameters == null) {
                throw new NullPointerException("parameters required");
            }
            if (parameters.size() != 2) {
                throw new IllegalArgumentException("snap( point, line) requires two parameters only");
            }
            this.parameters = parameters;
            this.fallback = fallback;
        }
    
        public Object evaluate(Object object) {
            return evaluate(object, Point.class);
        }
    
        public <T> T evaluate(Object object, Class<T> context) {
            Expression pointExpression = parameters.get(0);
            Point point = pointExpression.evaluate(object, Point.class);
    
            Expression lineExpression = parameters.get(1);
            Geometry line = lineExpression.evaluate(object, Geometry.class);
    
            LocationIndexedLine index = new LocationIndexedLine(line);
    
            LinearLocation location = index.project(point.getCoordinate());
    
            Coordinate snap = index.extractPoint(location);
    
            Point pt = point.getFactory().createPoint(snap);
    
            return Converters.convert(pt, context); // convert to requested format
        }
    
        public Object accept(ExpressionVisitor visitor, Object extraData) {
            return visitor.visit(this, extraData);
        }
    
        public String getName() {
            return NAME.getName();
        }
    
        public FunctionName getFunctionName() {
            return NAME;
        }
    
        public List<Expression> getParameters() {
            return parameters;
        }
    
        public Literal getFallbackValue() {
            return fallback;
        }
    }
    
  2. The mechanics of using a LocationIndexLine are covered in Snap a Point to a Line if you are interested.

  3. One thing to notice is the definition of FunctionName used to describe valid parameters to user of our new function.

    By convention we define this as a static final SnapFunction.NAME, this is however only a convention (which will help in implementing the next section).

  4. Create ExampleFunctionFactory implementing FunctionFactory

  5. Fill in the information as shown:

    /*
     *    GeoTools Sample code and Tutorials by Open Source Geospatial Foundation, and others
     *    https://docs.geotools.org
     *
     *    To the extent possible under law, the author(s) have dedicated all copyright
     *    and related and neighboring rights to this software to the public domain worldwide.
     *    This software is distributed without any warranty.
     *
     *    You should have received a copy of the CC0 Public Domain Dedication along with this
     *    software. If not, see <http://creativecommons.org/publicdomain/zero/1.0/>.
     */
    package org.geotools.tutorial.function;
    
    import java.util.ArrayList;
    import java.util.Collections;
    import java.util.List;
    import org.geotools.api.feature.type.Name;
    import org.geotools.api.filter.capability.FunctionName;
    import org.geotools.api.filter.expression.Expression;
    import org.geotools.api.filter.expression.Function;
    import org.geotools.api.filter.expression.Literal;
    import org.geotools.feature.NameImpl;
    import org.geotools.filter.FunctionFactory;
    
    public class ExampleFunctionFactory implements FunctionFactory {
    
        public List<FunctionName> getFunctionNames() {
            List<FunctionName> functionList = new ArrayList<>();
            functionList.add(SnapFunction.NAME);
            return Collections.unmodifiableList(functionList);
        }
    
        public Function function(String name, List<Expression> args, Literal fallback) {
            return function(new NameImpl(name), args, fallback);
        }
    
        public Function function(Name name, List<Expression> args, Literal fallback) {
            if (SnapFunction.NAME.getFunctionName().equals(name)) {
                return new SnapFunction(args, fallback);
            }
            return null; // we do not implement that function
        }
    }
    
  6. We make reference to the static final SnapFunction.NAME.

    While we mentioned this as only a convention, you are free to create a new FunctionNameImpl("snap", "point", "line") as part of the getFunctionNames() method. This has the advantage of avoiding loading SnapFunction until a user requests it by name.

  7. We can now register our factory by adding a file to our jar under META-INF/services/.

    Create the following file for inclusion in your jar:

    • src/main/resources/META-INF/services/org.geotools.filter.FunctionFactory

    Maven collections the contents of src/main/resources into our jar,

  8. Fill in the following contents (one implementation class per line):

    org.geotools.tutorial.function.ExampleFunctionFactory
    
  9. That is it SnapFunction is now published!

    You can use the “snap” function from your SLD documents; or in normal java programs.

Things to Try

  • Create a quick test case to show that the above function is available using:

    FilterFactory ff = CommonFactoryFinder.getFilterFactory();
    Expression expr = ff.function("snap", ff.property("the_geom"), ff.literal(lines));
    
  • The function will be very slow as written (when called to snap thousands of points).

    Have a check if you can detect the use of a literal MultiLineStinrg; and cache your LocationIndexedLine between calls to the function. A lot of GeoTools functions have been optimized in this fashion.

  • A fair bit of the code above is “boilerplate” and could be simplified with an appropriate AbstractFunction class.

    GeoTools does provide a couple abstract classes you can extend when defining your own functions. Have a look FunctionImpl and see if you find it easier then just starting from scratch.

  • Functions are used to process their parameters and produce an answer:

    public Object evaluate(Object feature) {
        Expression geomExpression = parameters.get(0);
        Geometry geom = geomExpression.evaulate(feature, Geometry.class);
    
        return geom.centroid();
    }
    

    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

    IMPORTANT: When using your function with a Style we try and be efficient. If the style calls your function without any reference to PropertyName it will only be called once. The assumption is that the function will produce the same value each time it is called, and thus will produce the same value for all features. By only calling the function once (and remembering the result) the rendering engine is able to perform much faster.

    If this is not the functionality you are after please implement VolatileFunction:

    public class MagicFunction extends FunctionExpressionImpl implements VolatileFunction {
        Random random;
        public MagicFunction() {
            super("magic");
            random = new Random();
        }
        public int getArgCount() {
            return 0; // no arguments!
        }
        public Object evaluate(Object feature) {
            float r = rand.nextFloat();
            float g = rand.nextFloat();
            float b = rand.nextFloat();
    
            Color color = new Color(r, g, b);
    
            return color;
        }
    }
    

Function

Normally we have a little background information on the concepts covered; in this case there is an article on how GeoTools uses factories; and the steps to consider when creating your own factory system for others to use.