I'm trying to apply a filter rule in an SLD, before applying the GeoServer PointStacker transformation (which groups points that are too close to distinguish their icons). This transformation is nicely described and demonstrated here and here etc.

Without my filter, the transformation is working fine, and without the transformation, my filter is working fine. My filter must go in the SLD instead of in CQL because it allows certain points to be conditionally displayed at certain zoom levels, depending on an attribute field. I can't seem to find where and how to put in the filter condition without breaking the XML validation (or even a related question on SE).

Below is what I have tried already. I've put the filter in after the transformation, so as to show what I'm trying to achieve. (Of course this does not work for two reasons: 1\ the point-stacker transformation does not return my original fields used in my filter, so the filter no longer works there. 2\ it stacks points which should have been filtered out already by the time the transformation is done.)

I've also tried moving the <ogc:filter> tag and contents to just before the <Transformation>, as well as just inside it - at the start or the end. All these attempts gave XML validation errors.

EDIT: removed fluff from the code here and formatted. The code is exactly as per the first link in par. 1 above, but with a filter added.

<?xml version="1.0" encoding="UTF-8"?>
<StyledLayerDescriptor version="1.0.0" xsi:schemaLocation="http://www.opengis.net/sld StyledLayerDescriptor.xsd" xmlns="http://www.opengis.net/sld" xmlns:ogc="http://www.opengis.net/ogc" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

        <Transformation>    <!-- exactly as per in the references above -->

          <ogc:Filter>      <!-- I want this filter to be applied before the Transformation  -->




GeoServer v2.7.2 with OpenLayers 3.9.0. The WPS extension is installed (required for the transformation).


2 Answers 2


I think the traditional answer is that you can't filter the data because it has already been calculated by the time it reaches the transformation process.

It might be possible to filter the data within the rule by chaining WPS functions. The WPS process you would need is probably the vec:Query (or gs:Query) function.

However I believe this is not currently available within the rendering transformations.


I got it decompile org.geotools.process.vector.PointStackerProcess from gt-process-feature-x.x.jar in webapps\geoserver\WEB-INF\lib and editing in the next way:

package org.geotools.process.vector;

import java.util.Collection;

import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import org.geotools.data.collection.ListFeatureCollection;
import org.geotools.data.simple.SimpleFeatureCollection;
import org.geotools.data.simple.SimpleFeatureIterator;
import org.geotools.feature.simple.SimpleFeatureBuilder;
import org.geotools.feature.simple.SimpleFeatureTypeBuilder;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.process.ProcessException;
import org.geotools.process.factory.DescribeParameter;
import org.geotools.process.factory.DescribeProcess;
import org.geotools.process.factory.DescribeResult;
import org.geotools.referencing.CRS;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.CoordinateSequenceFactory;
import org.locationtech.jts.geom.Envelope;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.Point;
import org.locationtech.jts.geom.Polygon;
import org.locationtech.jts.geom.impl.PackedCoordinateSequenceFactory;
import org.opengis.feature.Property;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.feature.type.AttributeDescriptor;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.TransformException;
import org.opengis.util.ProgressListener;

@DescribeProcess(title = "Point Stacker", description = "Aggregates a collection of points over a grid into one point per grid cell.")
public class PointStackerProcess implements VectorProcess {

    public static final String ATTR_GEOM = "geom";

    public static final String ATTR_COUNT = "count";

    public static final String ATTR_COUNT_UNIQUE = "countunique";

    public static final String ATTR_BOUNDING_BOX_GEOM = "geomBBOX";

    public static final String ATTR_BOUNDING_BOX = "envBBOX";

    public static final String ATTR_NORM_COUNT = "normCount";

    public static final String ATTR_NORM_COUNT_UNIQUE = "normCountUnique";

    public enum PreserveLocation {
        Single, Superimposed, Never;

    @DescribeResult(name = "result", description = "Aggregated feature collection")
    public SimpleFeatureCollection execute(@DescribeParameter(name = "data", description = "Input feature collection") SimpleFeatureCollection data, @DescribeParameter(name = "cellSize", description = "Grid cell size to aggregate to, in pixels") Integer cellSize, @DescribeParameter(name = "weightClusterPosition", description = "Weight cluster position based on points added", defaultValue = "false") Boolean argWeightClusterPosition, @DescribeParameter(name = "normalize", description = "Indicates whether to add fields normalized to the range 0-1.", defaultValue = "false") Boolean argNormalize, @DescribeParameter(name = "preserveLocation", description = "Indicates wheter to preserve the original location of points for single/superimposed points", defaultValue = "Never", min = 0) PreserveLocation preserveLocation, @DescribeParameter(name = "outputBBOX", description = "Bounding box for target image extent") ReferencedEnvelope outputEnv, @DescribeParameter(name = "outputWidth", description = "Target image width in pixels", minValue = 1.0D) Integer outputWidth, @DescribeParameter(name = "outputHeight", description = "Target image height in pixels", minValue = 1.0D) Integer outputHeight, ProgressListener monitor) throws ProcessException, TransformException {
        CoordinateReferenceSystem srcCRS = ((SimpleFeatureType) data.getSchema()).getCoordinateReferenceSystem();
        CoordinateReferenceSystem dstCRS = outputEnv.getCoordinateReferenceSystem();
        MathTransform crsTransform = null;
        MathTransform invTransform = null;
        try {
            crsTransform = CRS.findMathTransform(srcCRS, dstCRS);
            invTransform = crsTransform.inverse();
        } catch (FactoryException e) {
            throw new ProcessException(e);
        boolean normalize = false;
        if (argNormalize != null) {
            normalize = argNormalize.booleanValue();
        boolean weightClusterPosition = false;
        if (argWeightClusterPosition != null) {
            weightClusterPosition = argWeightClusterPosition.booleanValue();
        double cellSizeSrc = cellSize.intValue() * outputEnv.getWidth() / outputWidth.intValue();
        Collection<StackedPoint> stackedPts = stackPoints(data, crsTransform, cellSizeSrc, weightClusterPosition, outputEnv
                .getMinX(), outputEnv
        SimpleFeatureType schema = null;
        for (StackedPoint sp : stackedPts) {
            schema = createType(srcCRS, normalize, sp.getFeature());
        ListFeatureCollection result = new ListFeatureCollection(schema);
        SimpleFeatureBuilder fb = new SimpleFeatureBuilder(schema);
        GeometryFactory factory = new GeometryFactory((CoordinateSequenceFactory) new PackedCoordinateSequenceFactory());
        double[] srcPt = new double[2];
        double[] srcPt2 = new double[2];
        double[] dstPt = new double[2];
        double[] dstPt2 = new double[2];
        int maxCount = 0;
        int maxCountUnique = 0;
        if (normalize) {
            for (StackedPoint sp : stackedPts) {
                if (maxCount < sp.getCount()) {
                    maxCount = sp.getCount();
                if (maxCountUnique < sp.getCount()) {
                    maxCountUnique = sp.getCountUnique();

        for (StackedPoint sp : stackedPts) {
            Coordinate pt = getStackedPointLocation(preserveLocation, sp);
            srcPt[0] = pt.x;
            srcPt[1] = pt.y;
            invTransform.transform(srcPt, 0, dstPt, 0, 1);
            Coordinate psrc = new Coordinate(dstPt[0], dstPt[1]);
            Point point = factory.createPoint(psrc);
            Envelope boundingBox = sp.getBoundingBox();
            srcPt[0] = boundingBox.getMinX();
            srcPt[1] = boundingBox.getMinY();
            srcPt2[0] = boundingBox.getMaxX();
            srcPt2[1] = boundingBox.getMaxY();
            invTransform.transform(srcPt, 0, dstPt, 0, 1);
            invTransform.transform(srcPt2, 0, dstPt2, 0, 1);
            Envelope boundingBoxTransformed = new Envelope(dstPt[0], dstPt[1], dstPt2[0], dstPt2[1]);

            if (normalize) {
                fb.add(Double.valueOf(sp.getCount() / maxCount));
                fb.add(Double.valueOf(sp.getCountUnique() / maxCountUnique));
            if (sp.getCount() == 1) {
                //Here when count is we add the attribute value to the transformed featured
                SimpleFeature ref = sp.getFeature();
                for (AttributeDescriptor ad : ref.getType().getAttributeDescriptors()) {
                    fb.set(ad.getType().getName(), ref.getAttribute(ad.getType().getName()));
        return (SimpleFeatureCollection) result;

    private Coordinate getStackedPointLocation(PreserveLocation preserveLocation, StackedPoint sp) {
        Coordinate pt = null;
        if (PreserveLocation.Single == preserveLocation) {
            if (sp.getCount() == 1) {
                pt = sp.getOriginalLocation();
        } else if (PreserveLocation.Superimposed == preserveLocation
                && sp.getCountUnique() == 1) {
            pt = sp.getOriginalLocation();
        if (pt == null) {
            pt = sp.getLocation();
        return pt;

    private Collection<StackedPoint> stackPoints(SimpleFeatureCollection data, MathTransform crsTransform, double cellSize, boolean weightClusterPosition, double minX, double minY) throws TransformException {
        SimpleFeatureIterator featureIt = data.features();
        Map<Coordinate, StackedPoint> stackedPts = new HashMap<>();
        double[] srcPt = new double[2];
        double[] dstPt = new double[2];
        Coordinate indexPt = new Coordinate();
        try {
            while (featureIt.hasNext()) {
                SimpleFeature feature = (SimpleFeature) featureIt.next();
                Geometry geom = (Geometry) feature.getDefaultGeometry();
                Coordinate p = getRepresentativePoint(geom);
                srcPt[0] = p.x;
                srcPt[1] = p.y;
                crsTransform.transform(srcPt, 0, dstPt, 0, 1);
                Coordinate pout = new Coordinate(dstPt[0], dstPt[1]);
                indexPt.x = pout.x;
                indexPt.y = pout.y;
                gridIndex(indexPt, cellSize);
                StackedPoint stkPt = stackedPts.get(indexPt);
                if (stkPt == null) {
                    double centreX = indexPt.x * cellSize + cellSize / 2.0D;
                    double centreY = indexPt.y * cellSize + cellSize / 2.0D;
                    stkPt = new StackedPoint(indexPt, new Coordinate(centreX, centreY));
                    stackedPts.put(stkPt.getKey(), stkPt);
                stkPt.add(pout, weightClusterPosition);
        } finally {
        return stackedPts.values();

    private static Coordinate getRepresentativePoint(Geometry g) {
        if (g.getNumPoints() == 1) {
            return g.getCoordinate();
        return g.getCentroid().getCoordinate();

    private void gridIndex(Coordinate griddedPt, double cellSize) {
        long ix = (long) (griddedPt.x / cellSize);
        long iy = (long) (griddedPt.y / cellSize);
        griddedPt.x = ix;
        griddedPt.y = iy;

    private SimpleFeatureType createType(CoordinateReferenceSystem crs, boolean stretch, SimpleFeature ref) {
        SimpleFeatureTypeBuilder tb = new SimpleFeatureTypeBuilder();
        tb.add("geom", Point.class, crs);
        tb.add("count", Integer.class);
        tb.add("countunique", Integer.class);
        tb.add("geomBBOX", Polygon.class);
        tb.add("envBBOX", String.class);
        if (stretch) {
            tb.add("normCount", Double.class);
            tb.add("normCountUnique", Double.class);
        //Here we add the attributes from the original feature
        if (ref != null) {
            for (AttributeDescriptor ad : ref.getType().getAttributeDescriptors()) {
        SimpleFeatureType sfType = tb.buildFeatureType();
        return sfType;

    private static class StackedPoint {

        //Here is where i save the last feature before transformation;
        private SimpleFeature feature;

        private Coordinate key;

        private Coordinate centerPt;

        private Coordinate location = null;

        private int count = 0;

        private Set<Coordinate> uniquePts;

        private Envelope boundingBox = null;

        public SimpleFeature getFeature() {
            return feature;

        public void setFeature(SimpleFeature feature) {
            this.feature = feature;

        public StackedPoint(Coordinate key, Coordinate centerPt) {
            this.key = new Coordinate(key);
            this.centerPt = centerPt;

        public Coordinate getKey() {
            return this.key;

        public Coordinate getLocation() {
            return this.location;

        public int getCount() {
            return this.count;

        public int getCountUnique() {
            if (this.uniquePts == null) {
                return 1;
            return this.uniquePts.size();

        public Envelope getBoundingBox() {
            return this.boundingBox;

        public void add(Coordinate pt) {
            add(pt, false);

        public void add(Coordinate pt, boolean weightClusterPosition) {
            if (this.uniquePts == null) {
                this.uniquePts = new HashSet<>();
            if (weightClusterPosition) {
            } else {
            if (this.boundingBox == null) {
                this.boundingBox = new Envelope();
            } else {

        public Coordinate getOriginalLocation() {
            if (this.uniquePts != null && this.uniquePts.size() == 1) {
                return this.uniquePts.iterator().next();
            return null;

        private void pickWeightedLocation(Coordinate pt) {
            if (this.location == null) {
                this.location = pt;
            this.location = average(this.location, pt);

        private void pickNearestLocation(Coordinate pt) {
            if (this.location == null) {
                this.location = average(this.centerPt, pt);
            if (pt.distance(this.centerPt) < this.location.distance(this.centerPt)) {
                this.location = average(this.centerPt, pt);

        private void pickCenterLocation(Coordinate pt) {
            if (this.location == null) {
                this.location = new Coordinate(pt);
            this.location = this.centerPt;

        private static Coordinate average(Coordinate p1, Coordinate p2) {
            double x = (p1.x + p2.x) / 2.0D;
            double y = (p1.y + p2.y) / 2.0D;
            return new Coordinate(x, y);

Then compile it again, replace in the jar file and start geoserver

enter image description here

  • 1
    You could have got the code directly from github and if this is a useful change proposed it to the dev team. That is the open source way
    – Ian Turton
    Commented Oct 29, 2021 at 15:51
  • Thank you, Antonio. I will examine you answer when I get a chance. Commented Nov 2, 2021 at 11:16

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.