0

I'm implementing a clustering solution using the PointStacker Transformation. It works great, but I would like to get information about individual features that the cluster point consists of.

I figure out that it is impossible to do this, because algorithm keeps only geom, count and countUnique in output. Are there any hacks to get this information? I found approaches based on server-side clustering with PostGIS dependent on zoom level, but in my case tables with points are editable and clusters must redrawn dynamically. The only idea is to use ST_ClusterDBSCAN inside each time I request to db or mouseclick with the map with PointStacker Transformation from Geoserver on client-side. However there are discrepancies in results caused by cellSize in pixels for PointStacker Transformation and distance in projection unit for ST_ClusterDBSCAN.

Any ideas?

EDIT:

my test cluster

this is my grouped by type and clustered data at scale ~20k. I would like to get the information about all the 2 (7) records in db. Moreover, my table with points is editable so the number of objects in cluster could change to 3 (8) records.

9
  • It makes no sense to show the details about the data if there are so many points that you need a cluster to show them - may be you need to summarize your data before displaying it
    – Ian Turton
    Apr 27, 2021 at 14:35
  • I actually need to show all the points in cluster and they are already grouped by type, so my clusters contains objects the same type
    – Jane
    Apr 27, 2021 at 14:38
  • then why cluster them?
    – Ian Turton
    Apr 27, 2021 at 15:00
  • There are many objects located closely to each other - on small scale it is difficult to distinguish them and I would like to use clusters
    – Jane
    Apr 27, 2021 at 19:35
  • either there are too many features so it makes no sense to display all the information or there aren't and you don't need a cluster - there's no way around this
    – Ian Turton
    Apr 28, 2021 at 7:42

1 Answer 1

0

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
                        .getMinY());
        SimpleFeatureType schema = null;
        for (StackedPoint sp : stackedPts) {
            schema = createType(srcCRS, normalize, sp.getFeature());
            break;
        }
        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);
            fb.add(point);
            fb.add(Integer.valueOf(sp.getCount()));
            fb.add(Integer.valueOf(sp.getCountUnique()));
            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]);
            fb.add(boundingBoxTransformed);
            fb.add(boundingBoxTransformed.toString());

            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()));
                }
            }
            result.add(fb.buildFeature(null));
        }
        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));
                    stkPt.setFeature(feature);
                    stackedPts.put(stkPt.getKey(), stkPt);
                }
                stkPt.add(pout, weightClusterPosition);
            }
        } finally {
            featureIt.close();
        }
        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()) {
                tb.add(ad);
            }
        }
        tb.setName("stackedPoint");
        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) {
            this.count++;
            if (this.uniquePts == null) {
                this.uniquePts = new HashSet<>();
            }
            this.uniquePts.add(pt);
            if (weightClusterPosition) {
                pickWeightedLocation(pt);
            } else {
                pickNearestLocation(pt);
            }
            if (this.boundingBox == null) {
                this.boundingBox = new Envelope();
            } else {
                this.boundingBox.expandToInclude(pt);
            }
        }

        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;
                return;
            }
            this.location = average(this.location, pt);
        }

        private void pickNearestLocation(Coordinate pt) {
            if (this.location == null) {
                this.location = average(this.centerPt, pt);
                return;
            }
            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);
                return;
            }
            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 GeosServer

enter image description here

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.