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}