How to create a new operator

An Operator is a class that may operate on data from a number of source data products and create a new data product as a result. An Operator can be used within the SNAP Graph Processing Framework (GPF). The GPF provides a fast and efficient way to execute operators, as computation is performed tile-wise.

A tile is a chunk of data. The full scene image is not processed at once, as this would lead to a lot of memory problems. So we split the data into rectangular tiles and process each tile individually. Multiple times may be processed simultaneously. You are requested to fill the target tile rectangle with data, but you are free to access any source data also outside of the target rectangle. Where you get the data from is up to you.

A GPF Operator extends the abstract class Operator. When you extend the Operator class, there are five methods that you must or may implement:

  • void initialize(): This method is called before any actual computing is performed. Its purpose is to validate the input parameters and source products. It is also responsible for creating the target product and its properties. It is the only method that is not optional to be implemented.

  • void computeTile(Tile targetTile, ProgressMonitor pm): This method is expected to compute and set values for a tile of a single band of the target product. The implementation of this method is optional. If it is implemented, computeTileStack should not be implemented.

  • void computeTileStack(Map<Band, Tile> targetTiles, Rectangle targetTileRectangle, ProgressMonitor pm): This method is expected to compute and set values for tiles of multiple bands of the target product. If it is implemented, computeTile should not be implemented,

  • void doExecute(ProgressMonitor pm): This method is optional. It may be used for time-consuming tasks that need to be conducted before the actual computation takes place or to actually do all the processing instead of computeTile() and computeTileStack().

  • void dispose(): This method is called after the target product has been computed. By implementing it you may release any resources the operator has been using to free cache.

To get a better understanding which methods you need to implement, look at https://senbox.atlassian.net/wiki/spaces/SNAP/pages/523927628 .

Project Structure

Create a module with a file structure as

1 2 3 4 5 6 7 8 9 10 11 12 my-module/ src/main/ java/<code-base>/*.java javahelp/* nbm/manifest.mf resources/META-INF/services/org.esa.snap.core.gpf.OperatorSpi resources/helpset.xml resources/layer.xml src/test/ java/<code-base>/*.java resources/<code-base>/* pom.xml

For convenience, you can clone the module from the template processor operator. Your Java Code should be based in my-module/src/main/java/<code-base>/, there you should place your Operator sub-class. Tests should be placed in my-module/src/test/java/<code-base>/, test resources in my-module/src/test/resources/<code-base>/. The meaning of the other files is explained in https://senbox.atlassian.net/wiki/spaces/SNAP/pages/24051787 .

Defining Operator Metadata

Operator Metadata is set via Annotations. In particular, you can use them to set an Operator Alias. Operator Aliases are used to identify the Operator in the GPF.

Example of Annotations used to set Metadata of the Mosaic Operator.

1 2 3 4 5 6 7 8 @OperatorMetadata(alias = "Mosaic", category = "Raster/Geometric Operations", version = "1.0", authors = "Marco Peters, Ralf Quast, Marco Zühlke", copyright = "(c) 2009 by Brockmann Consult", description = "Creates a mosaic out of a set of source products.", internal = false) public class MosaicOp extends Operator

Additionally, it is necessary to define source products, parameters, and a target product. This is also achieved through Annotations. You have to define a field, then set the appropriate Annotation. The GPF will take care of that the actual values are assigned correctly when the Operator is executed. Also, it interpretes these fields so that even a default GUI may be derived (in case your parameters have basic data types as Strings or Integers).

Defining Source Products and Target Products

When you annotate your source product, you may also define how many source products you expect. In the example below, the count attribute says that you expect exactly three source products. You may use both the @SourceProductsand the @SourceProductannotation, either of them, or none, if your operator does not require a source product. In your code, please use the methods getSourceProduct() or getSourceProducts() to guarantee that the field is set.

Examplary Use of Annotations to define the Source Products of an Operator.

1 2 3 4 5 @SourceProducts(count = 3, description = "The source products to be used for mosaicking.") Product[] sourceProducts; @SourceProduct(description = "A product to be updated.", optional = true) Product updateProduct;

Likewise, you need to set the @TargetProduct annotation to a target product field. You need to set exactly one target product.

Annotating a target product

1 2 @TargetProduct(description = "The target product of this operator.") Product targetProduct;

Defining Operator Parameters

Finally, you need to specify each parameter of your operator with a @Parameter annotation to be recognized as a parameter by the GPF.

Examples for Parameter Annotations

1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Parameter(defaultValue = "EPSG:4326", description = "The CRS of the target product, represented as WKT or authority code.") String crs; @Parameter(alias = "resampling", label = "Resampling Method", description = "The method used for resampling.", valueSet = {"Nearest", "Bilinear", "Bicubic"}, defaultValue = "Nearest") String resamplingName; @Parameter(description = "The southern latitude.", interval = "[-90,90]", defaultValue = "35.0", "unit" = "°") double southBound; @Parameter(description = "An ESRI shapefile, providing the considered geographical region(s) given as polygons. " + "If null, all pixels are considered.", notNull = false) File shapefile;

The example above shows several uses of the @Parameter annotation. You may use it to describe the parameter (while this is optional, please set this attribute always). You may set default values, alternative names, labels (the names of the parameters as they appear in User Interfaces), units, sets of allowed values the parameter might have, or intervals (in the case of numeric parameters). Use notNull to specify that the parameter is allowed to be null.

Below is an example for a Parameter Annotation using a complex, dedicated class. This class must then define its own parameters. By this manner it is possible to specify a set of parameters that from a logical unit or that a user may want to set an arbitrary number of times (both cases apply to the example). Note that if you use such complex parameters you will not be able to use the Default User Interface.

Examples for a Parameter Annotation using a complex class.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @Parameter(description = "The band configurations. These configurations determine the input of the operator.", alias = "bandConfigurations", notNull = true) BandConfiguration[] bandConfigurations; public class BandConfiguration { @Parameter(description = "The name of the band in the source products. If empty, parameter 'expression' must be provided.") public String sourceBandName; @Parameter(description = "The band maths expression serving as input band. If empty, parameter 'sourceBandName' must be provided.") public String expression; @Parameter(description = "The band maths expression serving as criterion for whether to consider pixels for computation.") public String validPixelExpression; @Parameter(description = "If true, from bands with integer values will be treated as categorical variables and measures will be" + "retrieved accordingly. (counts per integer value, names of two most frequent integer values). " + "If the band is an index band, class names will be extracted from it.", defaultValue = "false") public boolean retrieveCategoricalStatistics; public BandConfiguration() { // used by DOM converter } }

For Convenience: The PixelOperator

When you create an Operator you should consider subclassing the class PixelOperator, which is a subclass of Operator. The PixelOperator has been designed to cover a case of operators that occurs very frequently, that is, that values are computed pixelwise and there is no need to consider the vicinity of a pixel. A PixelOperator operates on Samples (Representations of Pixel Values for a Band) rather than Tiles. If you want to extend this abstract class, you need to implement the following methods:

  • void prepareInputs(): Is called instead of initalize(). Use this to validate input parameters and do computationally non-expensive preparation tasks. The initialization of the target product is taken care of by an underlying implementation of initialize(), though you may override the methods createTargetProduct(), if you like. The implementation of this method is optional.

  • void configureTargetProduct(): Sets the properties of the target product. This method is already implemented in a super class, but when you, e.g., want to add bands to the target product, you should override this method and do it here.

  • void configureSourceSamples(SourceSampleConfigurer sampleConfigurer): Use this method to define the samples you need from your source product(s), using the SourceSampleConfigurer (see below). The implementation of this method is mandatory.

  • void configureTargetSamples(TargetSampleConfigurer sampleConfigurer): Use this method to define the samples you want to add to your target product, using the TargetSampleConfigurer (see below). The implementation of this method is mandatory.

  • void computePixel(int x, int y, Sample[] sourceSamples, WritableSample[] targetSamples): With this method you compute the values of the previously defined target samples on base of the previously defined source samples for the pixel at location x, y.

The use of SampleConfigurers is explained below. You need to assign to each sample a unique index (the first parameter). SourceSamples and TargetSamples have their own sets of indexes. The indexes will specify the value positions in the sourceSamples and targetSamples arrays in computePixel(). When defining samples, you can either state to use a raster data node of a source product, and in case you have multiple source products, by defining the source product, or you can create a band maths expression, if you like even across multiple source products (note the syntax when referring bands from separate products). For defining target samples, you simply need to specify which raster band of the target product shall be accessible under which index.

Examples for defining Samples

1 2 3 4 5 6 sourceSampleConfigurer.defineSample(0, "radiance_0") sourceSampleConfigurer.defineSample(1, "radiance_1", product_0) sourceSampleConfigurer.defineComputedSample(2, ProductData.TYPE_INT16, "abs(vza - vaa)") sourceSampleConfigurer.defineComputedSample(3, ProductData.TYPE_INT16, "$1.radiance_2 + $2.correction", product_0, product_1) targetSampleConfigurer.defineSample(0, "chlorophyll")

Note that when using a PixelOperator , doExecute() and dispose() still serve the purposes as described above.

After creating an Operator, make sure you follow the instructions in https://senbox.atlassian.net/wiki/spaces/SNAP/pages/24051787 so that it is fully integrated into the GPF and SNAP Desktop.