001/*
002 *    GeoTools - The Open Source Java GIS Toolkit
003 *    http://geotools.org
004 *
005 *    (C) 2018, Open Source Geospatial Foundation (OSGeo)
006 *
007 *    This library is free software; you can redistribute it and/or
008 *    modify it under the terms of the GNU Lesser General Public
009 *    License as published by the Free Software Foundation;
010 *    version 2.1 of the License.
011 *
012 *    This library is distributed in the hope that it will be useful,
013 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
014 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
015 *    Lesser General Public License for more details.
016 *
017 */
018package org.kepler.gis.actor.calc;
019
020import it.geosolutions.jaiext.jiffle.Jiffle;
021import it.geosolutions.jaiext.jiffle.JiffleException;
022import it.geosolutions.jaiext.jiffle.parser.node.Band;
023import it.geosolutions.jaiext.jiffle.parser.node.Expression;
024import it.geosolutions.jaiext.jiffle.parser.node.GetSourceValue;
025import it.geosolutions.jaiext.jiffle.parser.node.ScalarLiteral;
026import it.geosolutions.jaiext.jiffle.runtime.BandTransform;
027import it.geosolutions.jaiext.jiffleop.JiffleDescriptor;
028import it.geosolutions.jaiext.jiffleop.JiffleRIF;
029import it.geosolutions.jaiext.range.Range.DataType;
030import java.awt.image.RenderedImage;
031import java.awt.image.SampleModel;
032import java.util.ArrayList;
033import java.util.Arrays;
034import java.util.Collections;
035import java.util.HashSet;
036import java.util.List;
037import java.util.Set;
038import java.util.logging.Level;
039import java.util.logging.Logger;
040import javax.media.jai.ImageLayout;
041import javax.media.jai.JAI;
042import javax.media.jai.ROI;
043import javax.media.jai.RenderedOp;
044import org.geotools.coverage.grid.GridCoverage2D;
045import org.geotools.coverage.grid.GridCoverageFactory;
046import org.geotools.coverage.grid.io.AbstractGridFormat;
047import org.geotools.coverage.grid.io.GridCoverage2DReader;
048import org.geotools.coverage.processing.operation.GridCoverage2DRIA;
049import org.geotools.factory.GeoTools;
050import org.geotools.image.jai.Registry;
051import org.geotools.process.ProcessException;
052import org.geotools.process.factory.DescribeParameter;
053import org.geotools.process.factory.DescribeProcess;
054import org.geotools.process.factory.DescribeResult;
055import org.geotools.process.raster.RasterProcess;
056import org.geotools.resources.coverage.CoverageUtilities;
057import org.geotools.util.logging.Logging;
058import org.opengis.coverage.grid.GridCoverageReader;
059import org.opengis.parameter.GeneralParameterValue;
060import org.opengis.parameter.ParameterValue;
061import org.opengis.parameter.ParameterValueGroup;
062import org.opengis.util.ProgressListener;
063
064@DescribeProcess(title = "Jiffle map algebra", description = "Map algebra powered by Jiffle")
065public class JiffleProcess implements RasterProcess {
066
067    static {
068        Registry.registerRIF(
069                JAI.getDefaultInstance(),
070                new JiffleDescriptor(),
071                new JiffleRIF(),
072                "it.geosolutions.jaiext");
073    }
074
075    static final Logger LOGGER = Logging.getLogger(JiffleProcess.class);
076
077    public static final String IN_COVERAGE = "coverage";
078    public static final String IN_SCRIPT = "script";
079    public static final String IN_DEST_NAME = "destName";
080    public static final String IN_SOURCE_NAME = "sourceName";
081    public static final String IN_OUTPUT_TYPE = "outputType";
082    public static final String OUT_RESULT = "result";
083    public static final String TX_BANDS = "bands";
084
085    /**
086     * Executes a Jiffle raster algebra. Check the {@link DescribeParameter} annotations for a
087     * description of the various arguments
088     *
089     * @param progressListener
090     * @return
091     * @throws ProcessException
092     */
093    @DescribeResult(name = OUT_RESULT, description = "The map algebra output")
094    public GridCoverage2D execute(
095            @DescribeParameter(name = IN_COVERAGE, description = "Source raster(s)")
096                    GridCoverage2D[] coverages,
097            @DescribeParameter(
098                        name = IN_SCRIPT,
099                        description = "The script performing the map algebra, in Jiffle language"
100                    )
101                    String script,
102            @DescribeParameter(
103                        name = IN_DEST_NAME,
104                        description =
105                                "Name of the output, as used in the script (defaults to 'dest' if not specified)",
106                        min = 0
107                    )
108                    String destName,
109            @DescribeParameter(
110                        name = IN_SOURCE_NAME,
111                        description =
112                                "Name of the inputs, as used in the script (default to src, src1, src2, ... if not specified)",
113                        min = 0
114                    )
115                    String[] sourceNames,
116            @DescribeParameter(
117                        name = IN_OUTPUT_TYPE,
118                        description =
119                                "Output data type, BYTE, USHORT, SHORT, INT, FLOAT, DOUBLE. Defaults to DOUBLE if not specified",
120                        min = 0
121                    )
122                    DataType dataType,
123            ProgressListener progressListener)
124            throws ProcessException, JiffleException {
125        if (coverages.length == 0) {
126            // we could remove this limit, but a few extra input parameters are needed (output
127            // raster size and output envelope, with CRS)
128            throw new IllegalArgumentException("Need at least one coverage in input");
129        }
130        // prepare the input rendered images
131        RenderedImage[] sources = new RenderedImage[coverages.length];
132        GridCoverage2D reference = coverages[0];
133        sources[0] = reference.getRenderedImage();
134        for (int i = 1; i < sources.length; i++) {
135            GridCoverage2D coverage = coverages[i];
136            double[] nodata = CoverageUtilities.getBackgroundValues(coverage);
137            ROI roi = CoverageUtilities.getROIProperty(coverage);
138            sources[i] =
139                    GridCoverage2DRIA.create(
140                            coverage, reference, nodata, GeoTools.getDefaultHints(), roi);
141        }
142
143        // in case we have optimized out band selection, need to remap the band access
144        BandTransform[] bandTransforms = null;
145        if (sources.length == 1) {
146            BandTransform bt =
147                    getRenderingTransformationBandTransform(script, sourceNames, sources[0]);
148            bandTransforms = new BandTransform[] {bt};
149        }
150
151        Integer awtDataType = dataType == null ? null : dataType.getDataType();
152        RenderedOp result =
153                JiffleDescriptor.create(
154                        sources,
155                        sourceNames,
156                        destName,
157                        script,
158                        null,
159                        awtDataType,
160                        null,
161                        bandTransforms,
162                        GeoTools.getDefaultHints());
163
164        GridCoverageFactory factory = new GridCoverageFactory(GeoTools.getDefaultHints());
165        return factory.create("jiffle", result, reference.getEnvelope());
166    }
167
168    private BandTransform getRenderingTransformationBandTransform(
169            String script, String[] sourceNames, RenderedImage source) throws JiffleException {
170        // were we able to determine which bands are needed?
171        int[] scriptBands = getTransformationBands(script, sourceNames);
172        if (scriptBands == null) {
173            return null;
174        }
175
176        // is there a mismatch between the available bands and the ones used in the script?
177        // if so assume bands mapping has taken place in the RT
178        int maxReadBand = Arrays.stream(scriptBands).max().getAsInt();
179        // build the mapping as a lookup table
180        final int[] map = new int[maxReadBand + 1];
181        boolean mapRequired = false;
182        for (int i = 0; i < scriptBands.length; i++) {
183            int scriptBand = scriptBands[i];
184            map[scriptBand] = i;
185            mapRequired |= scriptBand != i;
186        }
187
188        if (!mapRequired) {
189            return null;
190        }
191
192        // perform a simple mapping using the above lookup table
193        return (x, y, scriptBand) -> map[scriptBand];
194    }
195
196    /**
197     * This is called by the renderer to optimize the read, if possible, we'll customize the band
198     * reading so that we read only what we know will be used by the script. At the time of writing,
199     * this works only if band positions in the script are literals.
200     */
201    public GeneralParameterValue[] customizeReadParams(
202            @DescribeParameter(
203                        name = IN_SCRIPT,
204                        description = "The script performing the map algebra, in Jiffle language"
205                    )
206                    String script,
207            @DescribeParameter(
208                        name = IN_DEST_NAME,
209                        description =
210                                "Name of the output, as used in the script (defaults to 'dest' if not specified)",
211                        min = 0
212                    )
213                    String destName,
214            @DescribeParameter(
215                        name = IN_SOURCE_NAME,
216                        description =
217                                "Name of the inputs, as used in the script (default to src, src1, src2, ... if not specified)",
218                        min = 0
219                    )
220                    String[] sourceNames,
221            @DescribeParameter(
222                        name = TX_BANDS,
223                        description = "Bands read by the transformation",
224                        min = 0
225                    )
226                    int[] usedBands,
227            GridCoverageReader reader,
228            GeneralParameterValue[] params) {
229        try {
230            // do we have a band selection parameter in the input?
231            ParameterValueGroup readerParams = reader.getFormat().getReadParameters();
232            ParameterValue<?> bands =
233                    readerParams.parameter(AbstractGridFormat.BANDS.getName(null));
234            if (bands == null) {
235                LOGGER.log(Level.FINE, "The reader does not support band selection, reading all");
236                return params;
237            }
238
239            // cannot do anything if we cannot access the sample model
240            if (!(reader instanceof GridCoverage2DReader)) {
241                LOGGER.log(Level.FINE, "The reader is not a 2D one, reading all bands");
242                return params;
243            }
244            GridCoverage2DReader r2d = (GridCoverage2DReader) reader;
245            ImageLayout layout = r2d.getImageLayout();
246            if (layout == null || layout.getSampleModel(null) == null) {
247                LOGGER.log(Level.FINE, "Cannot determine the reader bands, reading them all");
248                return params;
249            }
250            SampleModel sampleModel = layout.getSampleModel(null);
251
252            // are we using less bands than available in the reader?
253            if (usedBands == null) {
254                usedBands = getTransformationBands(script, sourceNames);
255            }
256            if (usedBands == null || usedBands.length >= sampleModel.getNumBands()) {
257                return params;
258            }
259
260            return mergeBandParam(params, usedBands);
261        } catch (Exception e) {
262            LOGGER.log(
263                    Level.INFO,
264                    "Failed to determine if we can read less bands based on the Jiffle script, continuing reading all source bands",
265                    e);
266            return params;
267        }
268    }
269
270    private GeneralParameterValue[] mergeBandParam(GeneralParameterValue[] params, int[] bands) {
271        // is it already there for some reason?
272        if (params != null) {
273            for (GeneralParameterValue param : params) {
274                if (param.getDescriptor().getName().equals(AbstractGridFormat.BANDS.getName())) {
275                    ((ParameterValue) param).setValue(bands);
276                    return params;
277                }
278            }
279        }
280
281        // not found, need to add
282        List<GeneralParameterValue> list =
283                new ArrayList<>(params == null ? Collections.emptyList() : Arrays.asList(params));
284        ParameterValue<int[]> value = AbstractGridFormat.BANDS.createValue();
285        value.setValue(bands);
286        list.add(value);
287        return list.toArray(new GeneralParameterValue[list.size()]);
288    }
289
290    /**
291     * Returns the source bands used, or null the bands indexes cannot be computed (e.g., they
292     * depend on script variables)
293     *
294     * @param script
295     * @param sourceNames
296     * @return
297     * @throws JiffleException
298     */
299    private int[] getTransformationBands(String script, String[] sourceNames)
300            throws JiffleException {
301        // a rendering transformation only uses a single input
302        String sourceName = "src";
303        if (sourceNames != null && sourceNames.length > 0) {
304            sourceName = sourceNames[0];
305        }
306
307        // get the reading positions
308        Set<GetSourceValue> positions = Jiffle.getReadPositions(script, Arrays.asList(sourceName));
309        if (positions.isEmpty()) {
310            return null;
311        }
312        // extract the bands, bail out if a band is specified through an expression
313        Set<Integer> bands = new HashSet<>();
314        for (GetSourceValue position : positions) {
315            Band band = position.getPos().getBand();
316            Expression index = band.getIndex();
317            if (index == null) {
318                bands.add(0);
319            } else if (index instanceof ScalarLiteral) {
320                bands.add(Integer.valueOf(((ScalarLiteral) band.getIndex()).getValue()));
321            } else {
322                if (LOGGER.isLoggable(Level.FINE)) {
323                    LOGGER.log(
324                            Level.FINE,
325                            "Cannot determine read bands, the source read spec use an expression "
326                                    + "for the band, not a literal: "
327                                    + position);
328                }
329                return null;
330            }
331        }
332
333        return bands.stream().mapToInt(b -> b).sorted().toArray();
334    }
335}