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 - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2019, 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.tutorial.function; import java.util.List; 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; import org.opengis.filter.capability.FunctionName; import org.opengis.filter.expression.Expression; import org.opengis.filter.expression.ExpressionVisitor; import org.opengis.filter.expression.Function; import org.opengis.filter.expression.Literal; /** * 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
LocationIndexLineare covered in Snap a Point to a Line if you are interested.One thing to notice is the definition of
FunctionNameused 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
ExampleFunctionFactoryimplementingFunctionFactoryFill in the information as shown:
/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2019, 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.tutorial.function; import java.util.ArrayList; import java.util.Collections; import java.util.List; import org.geotools.feature.NameImpl; import org.geotools.filter.FunctionFactory; import org.opengis.feature.type.Name; import org.opengis.filter.capability.FunctionName; import org.opengis.filter.expression.Expression; import org.opengis.filter.expression.Function; import org.opengis.filter.expression.Literal; 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 loadingSnapFunctionuntil 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/resourcesinto our jar,Fill in the following contents (one implementation class per line):
org.geotools.tutorial.function.ExampleFunctionFactory
That is it
SnapFunctionis 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 yourLocationIndexedLinebetween 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
AbstractFunctionclass.GeoTools does provide a couple abstract classes you can extend when defining your own functions. Have a look
FunctionImpland 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
Styleusers often want to calculate a value based on the attributes of the Feature being drawn. TheExpressionPropertyNameis used in this fashion to extract values out of aFeatureand pass them into the function for evaluationIMPORTANT: When using your function with a
Stylewe try and be efficient. If the style calls your function without any reference toPropertyNameit 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.