class ContourProcess implements RasterProcess {

    @DescribeResult(name = "result", description = "Contour line features.  Contour level is in value attribute.")
    public SimpleFeatureCollection execute(
            @DescribeParameter(name = "data", description = "Input raster") GridCoverage2D gc2d,
            @DescribeParameter(name = "band", description = "Name of band to use for values to be contoured", min = 0, max = 1) Integer band,
            @DescribeParameter(name = "levels", description = "Values of levels at which to generate contours") double[] levels,
            @DescribeParameter(name = "interval", description = "Interval between contour values (ignored if levels parameter is supplied)", min = 0, minValue = 0) Double interval,
            @DescribeParameter(name = "simplify", description = "Indicates whether contour lines are simplified", min = 0) Boolean simplify,
            @DescribeParameter(name = "smooth", description = "Indicates whether contour lines are smoothed using Bezier smoothing", min = 0) Boolean smooth,
            @DescribeParameter(name = "roi", description = "Geometry delineating the region of interest (in raster coordinate system)", min = 0) Geometry roi,
            ProgressListener progressListener) throws ProcessException {

        //
        // initial checks
        //
        if (gc2d == null) {
            throw new ProcessException("Invalid input, source grid coverage should be not null");
        }
        if (band != null && (band < 0 || band >= gc2d.getNumSampleDimensions())) {
            throw new ProcessException("Invalid input, invalid band number:" + band);
        }
        boolean hasValues = !(levels == null || levels.length == 0);
        if (!hasValues && interval == null) {
            throw new ProcessException("One between interval and values must be valid");

        }

        // switch to geophisics if necessary
        gc2d = gc2d.view(ViewType.GEOPHYSICS);

        //
        // GRID TO WORLD preparation
        //
        final AffineTransform mt2D = (AffineTransform) gc2d.getGridGeometry().getGridToCRS2D(
                PixelOrientation.CENTER);

        // get the list of nodata, if any
        List<Object> noDataList = new ArrayList<Object>();
        for (GridSampleDimension sd : gc2d.getSampleDimensions()) {
            // grab all the explicit nodata
            final double[] sdNoData = sd.getNoDataValues();
            if (sdNoData != null) {
                for (double nodata : sdNoData) {
                    noDataList.add(nodata);
                }
            }

            // handle also readers setting up nodata in a category with a specific name
            if (sd.getCategories() != null) {
                for (Category cat : sd.getCategories()) {
                    if (cat.getName().equals(NO_DATA)) {
                        final NumberRange<? extends Number> catRange = cat.getRange();
                        if (!Double.isNaN(catRange.getMinimum())) {
                            if (catRange.getMinimum() == catRange.getMaximum()) {
                                noDataList.add(catRange.getMinimum());
                            } else {
                                Range<Double> noData = new Range<Double>(catRange.getMinimum(),
                                        catRange.isMinIncluded(), catRange.getMaximum(),
                                        catRange.isMaxIncluded());
                                noDataList.add(noData);
                            }
                        }
                    }
                }
            }
        }

        // get the rendered image
        final RenderedImage raster = gc2d.getRenderedImage();

        // perform jai operation
        ParameterBlockJAI pb = new ParameterBlockJAI("Contour");
        pb.setSource("source0", raster);

        if (roi != null) {
            pb.setParameter("roi", CoverageUtilities.prepareROI(roi, mt2D));
        }
        
        if (band != null) {
            pb.setParameter("band", band);
        }
        
        if (levels != null && levels.length > 0) {
            final ArrayList<Double> elements = new ArrayList<Double>(levels.length);
            for (double level : levels)
                elements.add(level);
            pb.setParameter("levels", elements);
        } else {
            pb.setParameter("interval", interval);
        }
        
        if (simplify != null) {
            pb.setParameter("simplify", simplify);
        }
        
        if (smooth != null) {
            pb.setParameter("smooth", smooth);
        }
        
        if (!noDataList.isEmpty()) {
            pb.setParameter("nodata", noDataList);
        }

        final RenderedOp dest = JAI.create("Contour", pb);
        @SuppressWarnings("unchecked")
        final Collection<LineString> prop = (Collection<LineString>) dest
                .getProperty(ContourDescriptor.CONTOUR_PROPERTY_NAME);

        // wrap as a feature collection and return
        final SimpleFeatureType schema = CoverageUtilities
                .createFeatureType(gc2d, LineString.class);
        final SimpleFeatureBuilder builder = new SimpleFeatureBuilder(schema);
        int i = 0;
        final ListFeatureCollection featureCollection = new ListFeatureCollection(schema);
        final AffineTransformation jtsTransformation = new AffineTransformation(mt2D.getScaleX(),
                mt2D.getShearX(), mt2D.getTranslateX(), mt2D.getShearY(), mt2D.getScaleY(),
                mt2D.getTranslateY());
        for (LineString line : prop) {

            // get value
            Double value = (Double) line.getUserData();
            line.setUserData(null);
            // filter coordinates in place
            line.apply(jtsTransformation);

            // create feature and add to list
            builder.set("the_geom", line);
            builder.set("value", value);

            featureCollection.add(builder.buildFeature(String.valueOf(i++)));

        }

        // return value

        return featureCollection;

    }

}