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¶
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; } }
The mechanics of using a
LocationIndexLine
are covered in Snap a Point to a Line if you are interested.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).Create
ExampleFunctionFactory
implementingFunctionFactory
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 } }
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 thegetFunctionNames()
method. This has the advantage of avoiding loadingSnapFunction
until a user requests it by name.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,Fill in the following contents (one implementation class per line):
org.geotools.tutorial.function.ExampleFunctionFactory
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 yourLocationIndexedLine
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. TheExpression
PropertyName
is used in this fashion to extract values out of aFeature
and pass them into the function for evaluationIMPORTANT: When using your function with a
Style
we try and be efficient. If the style calls your function without any reference toPropertyName
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.