class PointStackerProcess implements VectorProcess {

    @DescribeResult(name = "result", description = "Aggregated feature collection")
    public SimpleFeatureCollection execute(

            // process data
            @DescribeParameter(name = "data", description = "Input feature collection") SimpleFeatureCollection data,

            // process parameters
            @DescribeParameter(name = "cellSize", description = "Grid cell size to aggregate to, in pixels") Integer cellSize,
            @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,

            // output image parameters
            @DescribeParameter(name = "outputBBOX", description = "Bounding box for target image extent") ReferencedEnvelope outputEnv,
            @DescribeParameter(name = "outputWidth", description = "Target image width in pixels", minValue = 1) Integer outputWidth,
            @DescribeParameter(name = "outputHeight", description = "Target image height in pixels", minValue = 1) Integer outputHeight,
            

            ProgressListener monitor) throws ProcessException, TransformException {

        CoordinateReferenceSystem srcCRS = 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;
        }

        // TODO: allow output CRS to be different to data CRS 
        // assume same CRS for now...
        double cellSizeSrc = cellSize * outputEnv.getWidth() / outputWidth;

        Collection<StackedPoint> stackedPts = stackPoints(data, crsTransform, cellSizeSrc,
                outputEnv.getMinX(), outputEnv.getMinY());

        SimpleFeatureType schema = createType(srcCRS, normalize);
        ListFeatureCollection result = new ListFeatureCollection(schema);
        SimpleFeatureBuilder fb = new SimpleFeatureBuilder(schema);

        GeometryFactory factory = new GeometryFactory(new PackedCoordinateSequenceFactory());

        double[] srcPt = new double[2];
        double[] dstPt = new double[2];

        // Find maxima of the point stacks if needed.
        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) {
            // create feature for stacked point
            Coordinate pt = getStackedPointLocation(preserveLocation, sp);
            
            // transform back to src CRS, since RT rendering expects the output to be in the same CRS
            srcPt[0] = pt.x;
            srcPt[1] = pt.y;
            invTransform.transform(srcPt, 0, dstPt, 0, 1);
            Coordinate psrc = new Coordinate(dstPt[0], dstPt[1]);

            Geometry point = factory.createPoint(psrc);
            fb.add(point);
            fb.add(sp.getCount());
            fb.add(sp.getCountUnique());
            if(normalize){
                fb.add(((double)sp.getCount())/maxCount);
                fb.add(((double)sp.getCountUnique())/maxCountUnique);
            }
            
            result.add(fb.buildFeature(null));
        }
        return result;
    }

}