/* * Geotoolkit.org - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2008-2012, Open Source Geospatial Foundation (OSGeo) * (C) 2009-2012, Geomatys * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. */ package org.geotoolkit.coverage.grid; import java.util.Map; import java.util.TreeMap; import java.util.Arrays; import java.util.LinkedHashMap; import java.util.List; import java.awt.Point; import java.awt.Dimension; import java.awt.Rectangle; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Color; import java.awt.Image; import java.awt.image.Raster; import java.awt.image.DataBufferFloat; import java.awt.image.ColorModel; import java.awt.image.SampleModel; import java.awt.image.RenderedImage; import java.awt.image.BufferedImage; import java.awt.image.WritableRaster; import java.awt.image.renderable.RenderableImage; import java.awt.geom.AffineTransform; import java.awt.geom.NoninvertibleTransformException; import java.io.IOException; import javax.measure.Unit; import javax.media.jai.*; import javax.media.jai.operator.ImageFunctionDescriptor; import org.opengis.geometry.Envelope; import org.opengis.geometry.MismatchedDimensionException; import org.opengis.util.FactoryException; import org.opengis.coverage.SampleDimension; import org.opengis.coverage.SampleDimensionType; import org.opengis.coverage.grid.GridCoverage; import org.opengis.coverage.grid.GridGeometry; import org.opengis.coverage.grid.GridEnvelope; import org.opengis.metadata.content.TransferFunctionType; import org.opengis.referencing.datum.PixelInCell; import org.opengis.metadata.spatial.PixelOrientation; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.operation.Matrix; import org.opengis.referencing.operation.MathTransform; import org.opengis.referencing.operation.MathTransform1D; import org.opengis.referencing.operation.TransformException; import org.geotoolkit.lang.Builder; import org.geotoolkit.util.Cloneable; import org.apache.sis.measure.NumberRange; import org.apache.sis.util.ArgumentChecks; import org.apache.sis.util.collection.BackingStoreException; import org.geotoolkit.factory.Hints; import org.apache.sis.measure.Units; import org.geotoolkit.coverage.Category; import org.geotoolkit.coverage.GridSampleDimension; import org.apache.sis.geometry.Envelopes; import org.apache.sis.geometry.GeneralEnvelope; import org.apache.sis.geometry.ImmutableEnvelope; import org.apache.sis.referencing.CRS; import org.apache.sis.referencing.operation.transform.MathTransforms; import org.apache.sis.internal.referencing.j2d.AffineTransform2D; import org.geotoolkit.metadata.iso.spatial.PixelTranslation; import org.geotoolkit.resources.Errors; import org.apache.sis.referencing.operation.transform.TransferFunction; import static java.awt.image.DataBuffer.*; import static org.apache.sis.util.collection.Containers.isNullOrEmpty; import org.geotoolkit.image.internal.ImageUtilities; import org.geotoolkit.image.palette.PaletteFactory; import org.apache.sis.referencing.crs.AbstractCRS; import org.apache.sis.referencing.cs.AxesConvention; /** * Helper class for the creation of {@link GridCoverage2D} instances. This builder can creates the * parameters to be given to {@linkplain GridCoverage2D#GridCoverage2D(CharSequence, PlanarImage, * GridGeometry2D, GridSampleDimension[], GridCoverage[], Map, Hints) grid coverage constructor} * from simpler parameters given to this builder. * <p> * The builder supports the following properties: * <p> * <table border="1" cellspacing="0" cellpadding="1"> * <tr bgcolor="lightblue"> * <th>Properties</th> * <th>Can be set from</th> * <th>Default value</th> * </tr><tr> * <td> {@link #crs} </td> * <td> {@linkplain #setCoordinateReferenceSystem(CoordinateReferenceSystem) CRS instance} or * {@linkplain #setCoordinateReferenceSystem(String) authority code} </td> * <td> {@linkplain Hints#DEFAULT_COORDINATE_REFERENCE_SYSTEM From hints} </td> * </tr><tr> * <td> {@link #envelope} </td> * <td> {@linkplain #setEnvelope(Envelope) Envelope instance} or * {@linkplain #setEnvelope(double[]) ordinate values} </td> * <td> </td> * </tr><tr> * <td> {@link #extent} </td> * <td> {@linkplain #setExtent(GridEnvelope) Grid envelope instance} or * {@linkplain #setExtent(int[]) spans} </td> * <td> {@linkplain #image Image} width and height </td> * </tr><tr> * <td> {@link #pixelAnchor} </td> * <td> {@linkplain #setPixelAnchor(PixelInCell) Code list value} </td> * <td> {@linkplain PixelInCell#CELL_CENTER Pixel center}</td> * </tr><tr> * <td> {@link #gridToCRS} </td> * <td> {@linkplain #setGridToCRS(MathTransform) Transform instance} or * {@linkplain #setGridToCRS(double, double, double, double, double, double) affine transform coefficients} </td> * <td> {@linkplain org.geotoolkit.referencing.operation.builder.GridToEnvelopeMapper Computed} from the above </td> * </tr><tr> * <td> {@link #gridGeometry} </td> * <td> {@linkplain #setGridGeometry(GridGeometry) Grid geometry instance} </td> * <td> {@linkplain GridGeometry2D#GridGeometry2D(GridEnvelope, PixelInCell, MathTransform, CoordinateReferenceSystem, Hints) Computed} from the above </td> * </tr><tr> * <td> {@link #numBands} </td> * <td> {@linkplain #setNumBands(int) Positive integer} </td> * <td> 1 </td> * </tr><tr> * <td> {@link Variable#name Variable.name} </td> * <td> {@linkplain Variable#setName(CharSequence) Character sequence} </td> * <td> </td> * </tr><tr> * <td> {@link Variable#unit Variable.unit} </td> * <td> {@linkplain Variable#setUnit(Unit) Unit instance} or * {@linkplain Variable#setUnit(String) unit symbol} </td> * <td> </td> * </tr><tr> * <td> {@link Variable#sampleRange Variable.sampleRange} </td> * <td> {@linkplain Variable#setSampleRange(NumberRange) Range instance} or * {@linkplain Variable#setSampleRange(int, int) lower and upper values} </td> * <td> </td> * </tr><tr> * <td> {@link Variable#sampleToUnit Variable.sampleToUnit} </td> * <td> {@linkplain Variable#setSampleToUnit(MathTransform1D) Transform instance} or * {@linkplain Variable#setLinearTransform(double, double) coefficients} </td> * <td> {@linkplain LinearTransform1D#IDENTITY Identity} </td> * </tr><tr> * <td> {@link Variable#colors Variable.colors} </td> * <td> {@linkplain Variable#setColors(Color[]) Colors array} or * {@linkplain Variable#setColors(String) palette name} </td> * <td> </td> * </tr><tr> * <td> {@link Variable#sampleDimension Variable.sampleDimension} </td> * <td> {@linkplain Variable#setSampleDimension(SampleDimension) Sample dimension instance} or * {@linkplain #setSampleDimensions(SampleDimension[]) array} </td> * <td> Computed from the above </td> * </tr><tr> * <td> Tile layout </td> * <td> {@linkplain #setTileSize(Dimension) Tile size} and/or * {@linkplain #setTileGridOffset(Point) tile grid offset} </td> * <td> </td> * </tr><tr> * <td> {@link #image} </td> * <td> {@linkplain #setRenderedImage(RenderedImage) Image}, * {@linkplain #setRenderedImage(WritableRaster) raster}, * {@linkplain #setRenderedImage(float[][]) matrix} or * {@linkplain #setRenderedImage(ImageFunction) function} </td> * <td> Empty image </td> * </tr><tr> * <td> {@link #sources} </td> * <td> {@linkplain #setSources(GridCoverage[]) Array of grid coverages} </td> * <td> </td> * </tr><tr> * <td> {@link #properties} </td> * <td> {@linkplain #setProperties(Map) Map of properties} </td> * <td> </td> * </tr> * </table> * * {@section Envelope vs <cite>grid to CRS</cite> transform} * The preferred way to define the geographic location of a grid coverage is to * {@linkplain #setGridToCRS(MathTransform) specify the grid to CRS transform} or the * {@linkplain #setGridGeometry(GridGeometry) grid geometry}. However as a convenience, * this builder also {@linkplain #setEnvelope(Envelope) accepts envelopes}. In such case, * this builder assumes that axis order in the supplied image matches exactly axis order in * the supplied envelope. In other words, in the usual case where axis order in the image is * (<var>column</var>, <var>row</var>), then the envelope should probably have a * (<var>longitude</var>, <var>latitude</var>) or (<var>easting</var>, <var>northing</var>) * axis order. * <p> * An exception to the above rule applies for CRS using exactly the following axis order: * ({@link org.opengis.referencing.cs.AxisDirection#NORTH NORTH}|{@link org.opengis.referencing.cs.AxisDirection#SOUTH SOUTH}, * {@link org.opengis.referencing.cs.AxisDirection#EAST EAST}|{@link org.opengis.referencing.cs.AxisDirection#WEST WEST}). * An example of such CRS is {@code EPSG:4326}. This builder will interchange automatically the * (<var>y</var>,<var>x</var>) axes for those CRS. * <p> * See {@link org.geotoolkit.referencing.operation.builder.GridToEnvelopeMapper} for more information * about the heuristic rules. If more control on axis order and direction reversal is wanted, specify * explicitely the <cite>grid to CRS</cite> transform or the <cite>grid geometry</cite> instead than * an envelope. * * {@section Examples} * Creates a grid coverage from the specified {@linkplain RenderedImage image} and * {@linkplain Envelope envelope}. An {@linkplain AffineTransform affine transform} will * be computed automatically from the specified envelope using heuristic rules described * in the above javadoc. * * {@preformat java * GridCoverageBuilder builder = new GridCoverageBuilder(); * builder.setName("My coverage"); // Optional * builder.setEnvelope(envelope); * builder.setRenderedImage(image); * GridCoverage2D coverage = builder.getGridCoverage2D(); * } * * Creates a grid coverage from the specified {@linkplain RenderedImage image} and * {@linkplain GridGeometry2D#getGridToCRS() grid to CRS} transform. In this example, * the pixels size is 1000×1000 metres and the CRS is <cite>Mercator</cite> ("EPSG:3395"). * The {@linkplain Envelope envelope} will be inferred from the grid geometry. * * {@preformat java * GridCoverageBuilder builder = new GridCoverageBuilder(); * builder.setName("My coverage"); // Optional * builder.setCoordinateReferenceSystem("EPSG:3395"); * builder.setGridToCRS(AffineTransform.getScaleInstance(1000, -1000)); * builder.setSampleDimensions(myFirstBand, mySecondBand); // Optional * builder.setRenderedImage(image); * GridCoverage2D coverage = builder.getGridCoverage2D(); * } * * Creates a 600×400 pixels image from -40°S to 40°N and -60°W to 60°E. * Uses the 0 pixel value for "<cite>no data</cite>", and pixel values in the * [1…255] range for elevation values in metres. Then draw something on * the image using Java2D API: * * {@preformat java * GridCoverageBuilder builder = new GridCoverageBuilder(); * builder.setCoordinateReferenceSystem("CRS:84"); * builder.setEnvelope(-60, -40, 60, 40); * builder.setExtent(600, 400); * * // Use sample values in the range 1 inclusive to 255 exclusive * // and define elevation in metres as (sample value) / 10. * builder.variable(0).setName("Elevation"); * builder.variable(0).setUnit(Units.METRE); * builder.variable(0).setSampleRange(1, 256); * builder.variable(0).setLinearTransform(0.1, 0); * builder.variable(0).addNodataValue("No data", 0, Color.GRAY); * * // Gets a 600×400 pixels (the extent) image, then draw something on it. * Graphics2D gr = (Graphics2D) builder.createGraphics(); * gr.draw(...); * gr.dispose(); * * // Gets the coverage. * GridCoverage2D coverage = builder.getGridCoverage2D(); * } * * @author Martin Desruisseaux (IRD, Geomatys) * @since 2.5 * * @version 3.20 * @module */ public class GridCoverageBuilder extends Builder<GridCoverage> { /** * The coverage name, or {@code null} if unspecified. This field is non-null only if the name * has been {@linkplain #setName(CharSequence) explicitely specified} by the user. The values * inferred from other attributes are not stored in this field. * * @see #getName() * @see #setName(CharSequence) * * @since 3.20 */ protected CharSequence name; /** * The coordinate reference system, or {@code null} if unspecified. This field is non-null only * if the CRS has been {@linkplain #setCoordinateReferenceSystem(CoordinateReferenceSystem) * explicitely specified} by the user. The values inferred from other attributes are not stored * in this field. * * @see #getCoordinateReferenceSystem() * @see #setCoordinateReferenceSystem(CoordinateReferenceSystem) * @see #setCoordinateReferenceSystem(String) * * @since 3.20 */ protected CoordinateReferenceSystem crs; /** * The envelope, including coordinate reference system, or {@code null} if unspecified. This * field is non-null only if the envelope has been {@linkplain #setEnvelope(Envelope) explicitely * specified} by the user. The values inferred from other attributes are not stored in this field. * * @see #getEnvelope() * @see #setEnvelope(Envelope) * @see #setEnvelope(double[]) * * @since 3.20 (derived from 2.5) */ protected Envelope envelope; /** * The grid extent, or {@code null} if unspecified. This field is non-null only if the extent * has been {@linkplain #setExtent(GridEnvelope) explicitely specified} by the user. The values * inferred from other attributes are not stored in this field. * * @see #getExtent() * @see #setExtent(GridEnvelope) * @see #setExtent(int[]) * * @since 3.20 */ protected GridEnvelope extent; /** * The <cite>grid to CRS</cite> transform, or {@code null} if unspecified. This field is non-null * only if the transform has been {@linkplain #setGridToCRS(MathTransform) explicitely specified} * by the user. The values inferred from other attributes are not stored in this field. * * @see #getGridToCRS() * @see #setGridToCRS(MathTransform) * @see #setGridToCRS(double, double, double, double, double, double) * * @since 3.20 */ protected MathTransform gridToCRS; /** * Whatever the {@link #gridToCRS} transform maps pixel center or pixel corner. This field is * non-null only if it has been {@linkplain #setPixelAnchor(PixelInCell) explicitely specified} * by the user. The values inferred from other attributes are not stored in this field. * * @see #getPixelAnchor() * @see #setPixelAnchor(PixelInCell) * * @since 3.20 */ protected PixelInCell pixelAnchor; /** * The grid geometry, or {@code null} if unspecified. This field is non-null only if the grid * geometry has been {@linkplain #setGridGeometry(GridGeometry) explicitely specified} by the * user. The values inferred from other attributes are not stored in this field. * * @see #getGridGeometry() * @see #setGridGeometry(GridGeometry) * * @since 3.20 */ protected GridGeometry gridGeometry; /** * The grid geometry calculated from other properties, or {@code null} if none. * * @since 3.20 */ private transient GridGeometry cachedGridGeometry; /** * Number of bands (sample dimensions), or 0 if unspecified. This field is non-zero only if * the number of bands has been {@linkplain #setNumBands(int) explicitely specified} by the * user, or {@linkplain #variable(int) variables were created}. The values inferred from other * attributes are not stored in this field. * * @see #getNumBands() * @see #setNumBands(int) * * @since 3.20 */ protected int numBands; /** * The list of variables created by the user, or {@code null} if unspecified. If non-null, * each element in this list will determine a {@linkplain GridSampleDimension grid sample * dimension} in the coverage to create. * * @see #variable(int) * * @since 3.20 (derived from 2.5) */ private Variable[] variables; /** * The sample dimensions created from the {@link #variables} array, or {@code null} * if none or not yet computed. If every sample dimensions are actually instances of * {@link GridSampleDimension}, then the array type is {@code GridSampleDimension[]}. * * @see #getSampleDimensions() * * @since 3.20 */ private transient SampleDimension[] sampleDimensions; /** * The {@linkplain #image} tile size and grid offset, or {@code null} if the image is untiled. * This field is non-null only if the tile size or grid offset has been explicitely specified * by the user. The values inferred from other attributes are not stored in this field. * <p> * The rectangle can be understood as the bounds of tile at index (0,0). Note that this tile * doesn't need to exist. * * @see #getTileSize() * @see #setTileSize(Dimension) * @see #getTileGridOffset() * @see #setTileGridOffset(Point) * * @since 3.20 */ private TileLayout tileLayout; /** * The aggregation of tile size and tile grid offset. This can be understood as the bounds * of the tile at tile index (0,0). Note that this tile doesn't need to exist. * * @since 3.20 */ @SuppressWarnings("serial") private static final class TileLayout extends Rectangle { /** * {@code true} if the grid offset is defined. If {@code false}, the * {@linkplain #x x} and {@linkplain #y y} ordinate values shall be ignored. */ private boolean hasOffset; /** * Creates a new tile layout initialized to the given grid tile offset. */ TileLayout(final Point location) { super(location); hasOffset = true; } /** * Creates a new tile layout initialized to the given tile size. */ TileLayout(final Dimension size) { super(size); } /** * Resets this tile layout to an "empty" state (i.e. all attributes are marked * as unspecified). */ public void reset() { x = 0; y = 0; width = 0; height = 0; hasOffset = false; } /** * Returns the tile grid offset only if defined, or {@code null} otherwise. */ @Override public Point getLocation() { return hasOffset ? super.getLocation() : null; } /** * Sets the tile grid offset to the given location. If the given argument is null, * unset the grid offset. */ @Override public void setLocation(final Point location) { if (hasOffset = (location != null)) { super.setLocation(location); } } /** * Returns the tile size only if non-empty, or {@code null} otherwise. */ @Override public Dimension getSize() { return (width > 0 && height > 0) ? super.getSize() : null; } /** * Sets the size to the given dimension. If the given argument is null, * then unset the tile size. */ @Override public void setSize(final Dimension size) { if (size != null) { ArgumentChecks.ensureStrictlyPositive("width", size.width); ArgumentChecks.ensureStrictlyPositive("height", size.height); super.setSize(size); } else { width = 0; height = 0; } } } /** * The raster, or {@code null} if none. This field takes a non-null value only for a * short period of time, during the call to {@link #setRenderedImage(WritableRaster)}. */ private Raster raster; /** * The rendered image, or {@code null} if not yet computed. This image can either be * {@linkplain #setRenderedImage(RenderedImage) specified explicitely} by the user or * created from other properties. * <p> * The preferred implementation class is {@link BufferedImage}. However in some cases this * builder may instantiate other kind of images, like {@link javax.media.jai.TiledImage}. * * @see #getRenderedImage() * @see #setRenderedImage(RenderedImage) * * @since 3.20 (derived from 2.5) */ protected RenderedImage image; /** * The grid coverage. Will be created only when first needed. */ private GridCoverage2D coverage; /** * An optional array of sources to be associated with the grid coverage, * or {@code null} if none. Those sources will be given to the * {@linkplain GridCoverage2D#GridCoverage2D(CharSequence, PlanarImage, GridGeometry2D, * GridSampleDimension[], GridCoverage[], Map, Hints) grid coverage constructor} without * any processing by this class. * * @see #getSources() * @see #setSources(GridCoverage[]) * * @since 3.20 */ protected GridCoverage[] sources; /** * An optional map of properties to be associated with the grid coverage, * or {@code null} if none. Those properties will be given to the * {@linkplain GridCoverage2D#GridCoverage2D(CharSequence, PlanarImage, GridGeometry2D, * GridSampleDimension[], GridCoverage[], Map, Hints) grid coverage constructor} without * any processing by this builder class. * * @see #getProperties() * @see #setProperties(Map) * * @since 3.20 */ protected Map<?,?> properties; /** * Optional hints for fetching factories, or {@code null} if none. Those hints can be * specified at {@linkplain #GridCoverageBuilder(Hints) builder construction time}. * * @since 3.20 */ protected final Hints hints; /** * Creates a uninitialized builder. All fields values are {@code null}. */ public GridCoverageBuilder() { this.hints = null; } /** * Creates a uninitialized builder using the given hints. * Hints of special interest are: * <p> * <ul> * <li>{@link Hints#DEFAULT_COORDINATE_REFERENCE_SYSTEM} - The CRS to use when none is * {@linkplain #setCoordinateReferenceSystem(CoordinateReferenceSystem) explicitely set}.</li> * <li>{@link Hints#SAMPLE_DIMENSION_TYPE} - specifies the {@link SampleDimensionType} to be * used at rendering time, which can be one of {@link SampleDimensionType#UNSIGNED_8BITS * UNSIGNED_8BITS} or {@link SampleDimensionType#UNSIGNED_16BITS UNSIGNED_16BITS}.</li> * <li>{@link Hints#TILE_ENCODING} - controls the compression to use for the serialization * of {@link GridCoverage2D} instances.</li> * </ul> * * @param hints Optional hints for fetching factories, or {@code null} if none. * * @since 3.20 */ public GridCoverageBuilder(final Hints hints) { if (isNullOrEmpty(hints)) { this.hints = null; } else { this.hints = new Hints(hints); } } /** * Returns the coverage name. * This method returns the first non-null value in the above choices, in preference order: * <p> * <ul> * <li>The value defined by the last call to {@link #setName(CharSequence)}.</li> * <li>In an iteration over all {@linkplain #variable(int) variables} (if any), the * first non-null value returned by {@link Variable#getName() Variable.getName()}.</li> * </ul> * <p> * As a consequence of the above, the {@linkplain GridCoverage2D#getName() coverage name} will * be the name of the first {@linkplain #getSampleDimensions() sample dimension}, unless a * coverage name is explicitely given. * * @return The coverage name, or {@code null}. * * @see GridCoverage2D#getName() * * @since 3.20 */ public CharSequence getName() { CharSequence value = name; if (value == null) { final Variable[] variables = this.variables; for (int i=0; i<numBands; i++) { final Variable var = variables[i]; if (var != null) { value = var.getName(); if (value != null) { break; } } } } return value; } /** * Sets the coverage name. The given name is typically (but not restricted to) a * {@link String} or {@link org.opengis.util.InternationalString} instance. * * @param name The new name, or {@code null}. * * @since 3.20 */ public void setName(final CharSequence name) { this.name = name; coverage = null; } /** * Convenience method for checking object dimension validity. * This method is usually invoked for argument checking. * * @param name The name of the argument to check. * @param dimension The object dimension. * @param expectedDimension The Expected dimension for the object. * @throws MismatchedDimensionException if the object doesn't have the expected dimension. */ private static void ensureDimensionMatch(final String name, final int dimension, final int expectedDimension) throws MismatchedDimensionException { if (dimension != expectedDimension) { throw new MismatchedDimensionException(Errors.format(Errors.Keys.MismatchedDimension_3, name, dimension, expectedDimension)); } } /** * Returns {@code true} if the property identified by the given flag is defined in the * grid geometry. If the grid geometry is not an instance of {@link GeneralGridGeometry}, * then only the {@code EXTENT} and {@code GRID_TO_CRS} properties are assumed defined. * * @param gridGeometry The grid geometry to test, or {@code null}. * @param flag One of the {@link GeneralGridGeometry} constants. * @return {@code true} if the given property is defined. */ private boolean isDefined(final int flag) { final GridGeometry gridGeometry = this.gridGeometry; if (gridGeometry == null) { return false; } if (gridGeometry instanceof GeneralGridGeometry) { return ((GeneralGridGeometry) gridGeometry).isDefined(flag); } else { return (flag & (GeneralGridGeometry.EXTENT | GeneralGridGeometry.GRID_TO_CRS)) == flag; } } /** * Returns the current coordinate reference system. * This method returns the first non-null value in the above choices, in preference order: * <p> * <ul> * <li>The value defined by the last call to {@link #setCoordinateReferenceSystem(CoordinateReferenceSystem)}.</li> * <li>The {@linkplain #envelope} CRS.</li> * <li>The {@linkplain #gridGeometry grid geometry} CRS.</li> * <li>The value of {@link Hints#DEFAULT_COORDINATE_REFERENCE_SYSTEM}.</li> * </ul> * * @return The current CRS, or {@code null} if unspecified and can not be inferred. * * @see #crs * @see Envelope#getCoordinateReferenceSystem() * @see GridGeometry2D#getCoordinateReferenceSystem() * @see GridCoverage2D#getCoordinateReferenceSystem() */ public CoordinateReferenceSystem getCoordinateReferenceSystem() { final CoordinateReferenceSystem crs = this.crs; if (crs == null) { // We do not need to check for non-null CRS because the setter methods in // this builder have automatically set the envelope CRS when possible. final Envelope envelope = this.envelope; if (envelope != null) { return envelope.getCoordinateReferenceSystem(); } if (isDefined(GeneralGridGeometry.CRS)) { return ((GeneralGridGeometry) gridGeometry).getCoordinateReferenceSystem(); } if (hints != null) { return (CoordinateReferenceSystem) hints.get(Hints.DEFAULT_COORDINATE_REFERENCE_SYSTEM); } } return crs; } /** * Sets the coordinate reference system to the specified value. If an envelope * has been {@linkplain #setEnvelope(Envelope) explicitely defined}, * it will be reprojected to the new CRS. * * {@section Precedence} * If a grid geometry has been {@linkplain #setGridGeometry(GridGeometry) explicitely set} * and {@linkplain GeneralGridGeometry#getCoordinateReferenceSystem() contains a CRS}, then * that later CRS will have precedence for the creation of {@link GridCoverage2D} instances. * * @param crs The new CRS to use, or {@code null}. * @throws IllegalArgumentException if the given CRS is illegal for the * {@linkplain #getEnvelope() current envelope}. * * @see #crs * @see Envelopes#transform(Envelope, CoordinateReferenceSystem) */ public void setCoordinateReferenceSystem(final CoordinateReferenceSystem crs) throws IllegalArgumentException { if (crs != null) { final int dimension = crs.getCoordinateSystem().getDimension(); final MathTransform gridToCRS = this.gridToCRS; if (gridToCRS != null) { ensureDimensionMatch("crs", dimension, gridToCRS.getSourceDimensions()); } /* * If a Geotk implementation of GridGeometry was defined, we need to extract the * its main fields (namely GridToCRS and extent) before to clear the geometry in * order to rebuild later a new instance with the new CRS. */ if (isDefined(GeneralGridGeometry.CRS)) { if (gridToCRS == null) { this.gridToCRS = gridGeometry.getGridToCRS(); pixelAnchor = null; } if (extent == null) { extent = gridGeometry.getExtent(); } gridGeometry = null; } else if (isDefined(GeneralGridGeometry.ENVELOPE)) { gridGeometry = null; } /* * Reproject the envelope, if needed. */ final Envelope oldEnvelope = envelope; if (oldEnvelope != null) { ensureDimensionMatch("crs", dimension, oldEnvelope.getDimension()); try { envelope = Envelopes.transform(oldEnvelope, crs); } catch (TransformException exception) { throw new IllegalArgumentException(Errors.format( Errors.Keys.IllegalCoordinateReferenceSystem), exception); } if (envelope.getCoordinateReferenceSystem() != crs) { envelope = new ImmutableEnvelope(crs, envelope); } } } this.crs = crs; // Assign only after success. gridGeometryChanged(); } /** * Sets the coordinate reference system to the specified authority code. This method gives a * preference to axes in (<var>longitude</var>, <var>latitude</var>) order. This convenience * method is equivalent to the following code (omitting exception handling): * * {@preformat java * setCoordinateReferenceSystem(AbstractCRS.castOrCopy(CRS.forCode(code)).forConvention(AxesConvention.RIGHT_HANDED)); * } * * See {@link #setCoordinateReferenceSystem(CoordinateReferenceSystem)} for information about * precedence. * * @param code The authority code of the CRS to use, or {@code null}. * @throws IllegalArgumentException if the given authority code is illegal. * * @see #crs * @see CRS#forCode(String) */ public void setCoordinateReferenceSystem(final String code) throws IllegalArgumentException { CoordinateReferenceSystem crs = null; if (code != null) try { crs = AbstractCRS.castOrCopy(CRS.forCode(code)).forConvention(AxesConvention.RIGHT_HANDED); } catch (FactoryException exception) { throw new IllegalArgumentException(Errors.format( Errors.Keys.IllegalCoordinateReferenceSystem), exception); } setCoordinateReferenceSystem(crs); } /** * Returns the current envelope. * This method returns the first non-null value in the above choices, in preference order: * <p> * <ul> * <li>The value defined by the last call to {@link #setEnvelope(Envelope)}.</li> * <li>The {@linkplain #gridGeometry grid geometry} envelope.</li> * </ul> * * @return A copy of the current envelope, or {@code null} if unspecified and can not be inferred. * * @see #envelope * @see GridGeometry2D#getEnvelope() * @see GridCoverage2D#getEnvelope() */ public Envelope getEnvelope() { final Envelope envelope = this.envelope; if (envelope != null) { if (envelope instanceof Cloneable) { return (Envelope) ((Cloneable) envelope).clone(); } } else { if (isDefined(GeneralGridGeometry.ENVELOPE)) { return ((GeneralGridGeometry) gridGeometry).getEnvelope(); } } return envelope; } /** * Sets the envelope to the specified value. If a CRS has been * {@linkplain #setCoordinateReferenceSystem(CoordinateReferenceSystem) explicitely defined}, * then the given envelope will be reprojected to that CRS. * <p> * <strong>This method is not recommended</strong>, since the creation of a grid coverage from * an envelope implies some arbitrary choices. Those arbitrary choices are implemented as * heuristic rules documented in this <a href="#overview">class javadoc</a>. The recommended * usage is to {@linkplain #setGridToCRS(MathTransform) specify the grid to CRS transform} or * the {@linkplain #setGridGeometry(GridGeometry) grid geometry} instead, and specify an * envelope only when the other information are not available. * * {@section Precedence} * If a grid geometry has been {@linkplain #setGridGeometry(GridGeometry) explicitely set} * and {@linkplain GeneralGridGeometry#getEnvelope() contains an envelope}, then that later * envelope will have precedence for the creation of {@link GridCoverage2D} instances. * * @param envelope The new envelope to use, or {@code null}. * @throws IllegalArgumentException if the envelope is illegal for the CRS. * * @see #envelope * @see Envelopes#transform(Envelope, CoordinateReferenceSystem) */ public void setEnvelope(final Envelope envelope) throws IllegalArgumentException { Envelope newValue = envelope; if (newValue != null) { final int dimension = newValue.getDimension(); final MathTransform gridToCRS = this.gridToCRS; if (gridToCRS != null) { ensureDimensionMatch("envelope", dimension, gridToCRS.getTargetDimensions()); } final CoordinateReferenceSystem crs = this.crs; if (crs != null) { ensureDimensionMatch("envelope", dimension, crs.getCoordinateSystem().getDimension()); try { newValue = Envelopes.transform(newValue, crs); } catch (TransformException exception) { throw new IllegalArgumentException(Errors.format( Errors.Keys.IllegalCoordinateReferenceSystem), exception); } if (newValue.getCoordinateReferenceSystem() != crs) { newValue = new ImmutableEnvelope(crs, newValue); } } if (newValue == envelope) { newValue = ImmutableEnvelope.castOrCopy(newValue); } } this.envelope = newValue; gridGeometryChanged(); } /** * Sets the envelope to the specified values, which must be the lower corner coordinates * followed by upper corner coordinates. The number of arguments provided shall be twice * the envelope dimension, and minimum shall not be greater than maximum. * <blockquote> * <b>Example:</b> * (<var>x</var><sub>min</sub>, <var>y</var><sub>min</sub>, <var>z</var><sub>min</sub>, * <var>x</var><sub>max</sub>, <var>y</var><sub>max</sub>, <var>z</var><sub>max</sub>) * </blockquote> * See {@link #setEnvelope(Envelope)} for information about recommended practices and * precedence. * * @param ordinates The ordinates of the new envelope to use, or {@code null}. * @throws IllegalArgumentException if the envelope is illegal. */ public void setEnvelope(final double... ordinates) throws IllegalArgumentException { GeneralEnvelope env = null; if (ordinates != null) { final CoordinateReferenceSystem crs = this.crs; if (crs != null) { env = new GeneralEnvelope(crs); } else { env = new GeneralEnvelope(ordinates.length / 2); } env.setEnvelope(ordinates); } setEnvelope(env); } /** * Returns the current grid extent. * This method returns the first non-null value in the above choices, in preference order: * <p> * <ul> * <li>The value defined by the last call to {@link #setExtent(GridEnvelope)}.</li> * <li>The {@linkplain #gridGeometry grid geometry} extent.</li> * <li>The {@linkplain #image} bounds (including {@linkplain RenderedImage#getMinX() minX} * and {@linkplain RenderedImage#getMinY() minY} values).</li> * </ul> * * @return The current grid extent, or {@code null} if unspecified and can not be inferred. * * @see #extent * @see GridGeometry2D#getExtent() * @see org.opengis.coverage.grid.Grid#getExtent() * * @since 3.20 */ public GridEnvelope getExtent() { final GridEnvelope extent = this.extent; if (extent != null) { if (extent instanceof Cloneable) { return (GridEnvelope) ((Cloneable) extent).clone(); } } else { if (isDefined(GeneralGridGeometry.EXTENT)) { return gridGeometry.getExtent(); } final RenderedImage image = this.image; if (image != null) { final Envelope envelope = this.envelope; return new GeneralGridEnvelope(image, getGridDimension(envelope != null ? envelope.getDimension() : 2)); } } return extent; } /** * Sets the grid extent to the specified value. * * {@section Precedence} * If a grid geometry has been {@linkplain #setGridGeometry(GridGeometry) explicitely set} * and {@linkplain GeneralGridGeometry#getExtent() contains an extent}, then that later extent * will have precedence for the creation of {@link GridCoverage2D} instances. * * @param extent The new grid extent to use, or {@code null}. * @throws MismatchedDimensionException If the extent dimension is not equal to the * <cite>grid to CRS</cite> source dimensions. * * @since 3.20 */ public void setExtent(final GridEnvelope extent) throws MismatchedDimensionException { if (extent != null) { final int dim = getGridDimension(-1); if (dim >= 0) { ensureDimensionMatch("extent", extent.getDimension(), dim); } } this.extent = extent; gridGeometryChanged(); } /** * Sets the grid extent to a grid envelope having the given span. * The {@linkplain GridEnvelope#getLow() low ordinate values} are set to 0. * <p> * This method is typically invoked for defining image dimension as below: * * {@preformat java * setExtent(width, height); * } * * See {@link #setExtent(GridEnvelope)} for information about precedence. * * @param span The span values for all dimensions, or {@code null}. * @throws MismatchedDimensionException If the arguments contain negative span values, or the * number of values is not equal to the <cite>grid to CRS</cite> source dimensions. * * @since 3.20 */ public void setExtent(final int... span) throws MismatchedDimensionException { setExtent(span != null ? new GeneralGridEnvelope(new int[span.length], span, false) : null); } /** * Returns the grid dimension, which is inferred from the {@link #gridGeometry} if possible, * or the {@link #gridToCRS} otherwise. The {@linkplain #extent} is not used because this * method is invoked by the {@code get/setExtent(...)} methods. */ private int getGridDimension(final int defaultValue) { MathTransform tr = null; final GridGeometry gridGeometry = this.gridGeometry; if (gridGeometry != null) { if (gridGeometry instanceof GeneralGridGeometry) { return ((GeneralGridGeometry) gridGeometry).getDimension(); } else { tr = gridGeometry.getGridToCRS(); } } if (tr == null) { if ((tr = gridToCRS) == null) { return defaultValue; } } return tr.getSourceDimensions(); } /** * Implementation of {@link #getAffineGridToCRS()} with control on the desired pixel orientation. * * @param orientation The desired orientation, or {@code null} for the default. * @param force Whatever to force the transform to have the specified orientation if we get the * transform from the {@link #gridToCRS} field rather than {@link #gridGeometry}. */ private AffineTransform getAffineGridToCRS(final PixelOrientation orientation, boolean force) throws IllegalStateException { MathTransform candidate = gridToCRS; if (candidate == null) { final GridGeometry2D gridGeometry = GridGeometry2D.castOrCopy(getGridGeometry(false)); if (gridGeometry == null || (!force && !gridGeometry.isDefined(GridGeometry2D.GRID_TO_CRS))) { return null; } final PixelInCell pixelAnchor = this.pixelAnchor; if (pixelAnchor == null) { candidate = gridGeometry.getGridToCRS2D(); } else { candidate = gridGeometry.getGridToCRS2D(orientation); } force = false; } if (candidate instanceof AffineTransform) { AffineTransform at = (AffineTransform) candidate; if (force) { final double offset = -0.5 - PixelTranslation.getPixelTranslation(getPixelAnchor()); if (offset != 0) { at = new AffineTransform(at); at.translate(offset, offset); } } return at; } throw new IllegalStateException(Errors.format(Errors.Keys.NotAnAffineTransform)); } /** * Returns two-dimensional part of the current <cite>grid to CRS</cite> affine transform. * This method gets the transform as documented in the {@link #getGridToCRS()} method, * except that it tries to extract only the two-dimensional component of that transform. * <p> * Whatever the returned transform maps pixel centers or pixel corners depends on the * {@link #pixelAnchor} value. * * @return The <cite>grid to CRS</cite> transform, or {@code null} if unspecified and can not * be inferred. * @throws IllegalStateException If a transform exists but that transform is not affine. * * @since 3.20 */ public AffineTransform getAffineGridToCRS() throws IllegalStateException { return getAffineGridToCRS(PixelTranslation.getPixelOrientation(pixelAnchor), false); } /** * Returns the current <cite>grid to CRS</cite> transform. * This method returns the first non-null value in the above choices, in preference order: * <p> * <ul> * <li>The value defined by the last call to {@link #setGridToCRS(MathTransform)}.</li> * <li>The {@linkplain #gridGeometry grid geometry} transform.</li> * </ul> * <p> * Whatever the returned transform maps pixel centers or pixel corners depends on the * {@link #pixelAnchor} value. * * @return The <cite>grid to CRS</cite> transform, or {@code null} if unspecified and can not * be inferred. * * @see #gridToCRS * @see GridGeometry2D#getGridToCRS() * * @since 3.20 */ public MathTransform getGridToCRS() { final MathTransform gridToCRS = this.gridToCRS; if (gridToCRS == null) { final GridGeometry gridGeometry = getGridGeometry(false); if (gridGeometry != null) { final PixelInCell pixelAnchor = this.pixelAnchor; if (pixelAnchor == null) { return gridGeometry.getGridToCRS(); } else if (gridGeometry instanceof GeneralGridGeometry) { return ((GeneralGridGeometry) gridGeometry).getGridToCRS(pixelAnchor); } } } return gridToCRS; } /** * Sets the <cite>grid to CRS</cite> transform. Whatever the transform maps pixel centers * or pixel corners depends on the {@link #pixelAnchor} value. * * {@section Restrictions} * <ul> * <li>The number of {@linkplain MathTransform#getSourceDimensions() source dimensions} * shall matches the grid {@linkplain #extent} dimensions (if any)</li> * <li>The number of {@linkplain MathTransform#getTargetDimensions() target dimensions} * shall matches the {@linkplain #crs} and {@linkplain #envelope} dimensions (if any).</li> * </ul> * * {@section Precedence} * If a grid geometry has been {@linkplain #setGridGeometry(GridGeometry) explicitely set} * and {@linkplain GeneralGridGeometry#getGridToCRS() contains a transform}, then that later * transform will have precedence for the creation of {@link GridCoverage2D} instances. * * @param gridToCRS The new <cite>grid to CRS</cite> transform, or {@code null}. * @throws MismatchedDimensionException If the given transform is invalid, for example if the * dimensions don't match. * * @since 3.20 */ public void setGridToCRS(final MathTransform gridToCRS) throws MismatchedDimensionException { if (gridToCRS != null) { final CoordinateReferenceSystem crs = this.crs; final GridEnvelope extent = this.extent; if (extent != null) { ensureDimensionMatch("gridToCRS", gridToCRS.getSourceDimensions(), extent.getDimension()); } final Envelope envelope = this.envelope; if (crs != null || envelope != null) { ensureDimensionMatch("gridToCRS", gridToCRS.getTargetDimensions(), (crs != null) ? crs.getCoordinateSystem().getDimension() : envelope.getDimension()); } } this.gridToCRS = gridToCRS; gridGeometryChanged(); } /** * Sets the <cite>grid to CRS</cite> transform from a matrix. Whatever the transform maps * pixel centers or pixel corners depends on the {@link #pixelAnchor} value. * <p> * See {@link #setGridToCRS(MathTransform)} for information about restrictions and precedence. * * @param gridToCRS The new <cite>grid to CRS</cite> transform, or {@code null}. * @throws MismatchedDimensionException If the current {@linkplain #extent} and * {@linkplain #envelope} (if any) are not two-dimensional. * * @since 3.20 */ public void setGridToCRS(final Matrix gridToCRS) throws MismatchedDimensionException { setGridToCRS(gridToCRS != null ? MathTransforms.linear(gridToCRS) : null); } /** * Sets the <cite>grid to CRS</cite> transform from a Java2D transform. Whatever the * transform maps pixel centers or pixel corners depends on the {@link #pixelAnchor} value. * <p> * See {@link #setGridToCRS(MathTransform)} for information about restrictions and precedence. * * @param gridToCRS The new <cite>grid to CRS</cite> transform, or {@code null}. * @throws MismatchedDimensionException If the current {@linkplain #extent} and * {@linkplain #envelope} (if any) are not two-dimensional. * * @since 3.20 */ public void setGridToCRS(final AffineTransform gridToCRS) throws MismatchedDimensionException { setGridToCRS(gridToCRS != null ? org.geotoolkit.referencing.operation.MathTransforms.linear(gridToCRS) : null); } /** * Sets the <cite>grid to CRS</cite> transform from the given affine transform coefficients. * Whatever the transform maps pixel centers or pixel corners depends on the {@link #pixelAnchor} * value. * <p> * See {@link #setGridToCRS(MathTransform)} for information about restrictions and precedence. * * @param m00 the X coordinate scaling. * @param m10 the Y coordinate shearing. * @param m01 the X coordinate shearing. * @param m11 the Y coordinate scaling. * @param m02 the X coordinate translation. * @param m12 the Y coordinate translation. * * @see AffineTransform2D * * @throws MismatchedDimensionException If the current {@linkplain #extent} and * {@linkplain #envelope} (if any) are not two-dimensional. * * @since 3.20 */ public void setGridToCRS(double m00, double m10, double m01, double m11, double m02, double m12) throws MismatchedDimensionException { setGridToCRS((MathTransform) new AffineTransform2D(m00, m10, m01, m11, m02, m12)); } /** * Returns whatever the {@linkplain #gridToCRS grid to CRS} transform maps pixel center or * pixel corner. The OGC 01-004 specification mandates pixel center, but some computation * are more convenient when mapping pixel corners. * * @return The "pixel in cell" policy. If unspecified, default value is * {@link PixelInCell#CELL_CENTER}. * * @see #pixelAnchor * @see GeneralGridGeometry#GeneralGridGeometry(GridEnvelope, PixelInCell, MathTransform, CoordinateReferenceSystem) * * @since 3.20 */ public PixelInCell getPixelAnchor() { final PixelInCell pixelAnchor = this.pixelAnchor; return (pixelAnchor != null) ? pixelAnchor : PixelInCell.CELL_CENTER; } /** * Sets the whatever the {@linkplain #gridToCRS grid to CRS} transform maps pixel center or * pixel corner. Note that this attribute has no effect if the {@link #gridToCRS} attribute * is not used (for example because the {@link #gridGeometry} attribute has precedence). * * @param anchor The new "pixel in cell" policy, or {@code null}. * * @since 3.20 */ public void setPixelAnchor(final PixelInCell anchor) { this.pixelAnchor = anchor; gridGeometryChanged(); } /** * Returns the current grid geometry. If no grid geometry has been * {@linkplain #setGridGeometry(GridGeometry) explicitly defined}, then this * method builds a default instance from the values returned by <u>one</u> of * the following set of getter methods: * <p> * <table> * <tr><th>Recommended</th><th> </th><th>Alternative</th></tr> * <tr><td valign="top"><ul> * <li>{@link #getExtent()}</li> * <li>{@link #getPixelAnchor()}</li> * <li>{@link #getGridToCRS()}</li> * <li>{@link #getCoordinateReferenceSystem()}</li> * </ul> * </td><td valign="top"><b>or</b></td><td valign="top"> * <ul> * <li>{@link #getExtent()}</li> * <li>{@link #getEnvelope()}</li> * </ul></td></tr></table> * <p> * Note that creation of grid geometries from the parameters listed in the right column use heuristic * rules documented {@linkplain GeneralGridGeometry#GeneralGridGeometry(GridEnvelope,Envelope) here}. * In order to keep grid geometry creations more determinist, we recommend to specify the parameters * listed in the left column instead. * * @return The grid geometry, or {@code null} if unspecified and can not be inferred. * * @see #gridGeometry * @see GridCoverage2D#getGridGeometry() * * @since 3.20 */ public GridGeometry getGridGeometry() { return getGridGeometry(true); } /** * Implementation of {@link #getGridGeometry()}. * * @param useGridToCRS {@code true} if this method is allowed to invoke {@link #getGridToCRS()}. * This flag is necessary for avoiding never-ending recursive method invocations. */ private GridGeometry getGridGeometry(final boolean useGridToCRS) { GridGeometry geom = gridGeometry; if (geom == null) { geom = cachedGridGeometry; if (geom == null) { final GridEnvelope extent = getExtent(); final MathTransform gridToCRS; if (useGridToCRS && (gridToCRS = getGridToCRS()) != null) { geom = new GridGeometry2D(extent, getPixelAnchor(), gridToCRS, getCoordinateReferenceSystem(), hints); } else { final Envelope envelope = getEnvelope(); if (extent != null || envelope != null) { // Its okay to have 1 null value. geom = new GridGeometry2D(extent, envelope); } } if (useGridToCRS) { // Do not cache the result if we were not allowed to invoke getGridToCRS() // since the result could have been different. cachedGridGeometry = geom; } } } return geom; } /** * Sets the grid geometry to the given value. If non-null, this value will have precedence over * the {@linkplain #crs}, {@linkplain #envelope}, {@linkplain #extent}, {@linkplain #gridToCRS * grid to CRS} and {@linkplain #pixelAnchor pixel anchor} attributes. * * {@note If this property is set to <code>null</code> in the intend to force computation of * a new grid geometry from the <code>envelope</code> value, then the caller will also * needs to ensure that the <code>gridToCRS</code> property is <code>null</code>.} * * @param geom The new grid geometry, or {@code null}. * * @since 3.20 */ public void setGridGeometry(final GridGeometry geom) { gridGeometry = geom; gridGeometryChanged(); } /** * Invoked when a property used for computing the grid geometry has been changed. * * @since 3.20 */ private void gridGeometryChanged() { cachedGridGeometry = null; coverage = null; } /** * Invoked when a property used for computing the sample dimensions has been changed. * * @since 3.20 */ final void sampleDimensionsChanged() { sampleDimensions = null; coverage = null; } /** * Returns the number of sample dimensions (bands). * This method returns the first defined value in the above choices, in preference order: * <p> * <ul> * <li>The value defined by the last call to {@link #setNumBands(int)}.</li> * <li>The highest <var>n</var>+1 value given to {@link #variable(int)}.</li> * <li>The number of bands in the {@linkplain #image}.</li> * <li>The default value 1.</li> * </ul> * * @return The number of sample dimensions (bands). * * @see #numBands * @see SampleModel#getNumBands() * * @since 3.20 */ public int getNumBands() { int n = numBands; if (n == 0) { final RenderedImage image = this.image; if (image != null) { final SampleModel sampleModel = image.getSampleModel(); if (sampleModel != null) { return sampleModel.getNumBands(); } } n = 1; } return n; } /** * Sets the number of sample dimensions (bands). If any {@linkplain #variable(int) variable} * existed at the index <var>n</var> or greater, they will be discarded. * * @param n Number of sample dimensions, or 0 to unspecify. * @throws IllegalArgumentException If the given value is negative. * * @since 3.20 */ public void setNumBands(final int n) throws IllegalArgumentException { ArgumentChecks.ensurePositive("n", n); final Variable[] variables = this.variables; if (variables != null && n < variables.length) { Arrays.fill(variables, numBands, variables.length, null); } numBands = n; sampleDimensionsChanged(); } /** * Returns the {@linkplain GridSampleDimension sample dimension} builder for the given * band index. If a {@code Variable} instance exists at the given index, it will be * returned. Otherwise a new instance will be created. * <p> * If the given band index is equals or greater than the {@linkplain #getNumBands() number * of sample dimensions}, then the later will be increased as needed. * * @param band The index of the sample dimension for which to get the variable. * @return The builder for the given sample dimension. * @throws IllegalArgumentException If the given band index is negative. * * @since 3.20 */ public Variable variable(final int band) throws IllegalArgumentException { ArgumentChecks.ensurePositive("band", band); Variable[] v = this.variables; if (v == null) { // A length of 4 is a good match for ARGB images - few images need more bands. variables = v = new Variable[Math.max(4, band+1)]; } else if (band >= v.length) { variables = v = Arrays.copyOf(v, Math.max(v.length*2, band+1)); } Variable var = v[band]; if (var == null) { v[band] = var = newVariable(band); } if (band >= numBands) { numBands = band+1; } return var; } /** * Invoked by the {@link #variable(int)} method when a new variable need to be created. * This method is a hook for subclasses that wish to instantiate their own * {@link Variable Variable} subclasses. The default implementation is: * * {@preformat java * return new Variable(band); * } * * @param band The index of the sample dimension for which to get the new variable. * @return The new variable. * * @since 3.20 */ protected Variable newVariable(final int band) { return new Variable(band); } /** * Gets the given image property as an array of double values, or {@code null} if none. * * @param name The image property name, typically {@code "minimum"} or {@code "maximum"}. * @return The property value, or {@code null} if none or not a {@code double[]} type. */ final double[] getArrayProperty(final String name) { final RenderedImage image = this.image; if (image != null) { final Object property = image.getProperty(name); if (property instanceof double[]) { return (double[]) property; } } return null; } /** * Creates defaults sample dimensions. This method should be invoked only when no sample * dimensions can be built from the {@linkplain #variables}. This may happen in any of the * following cases: * <p> * <ul> * <li>There is no {@linkplain #variables}.</li> * <li>The variables do not contain "no data" value or range of sample values. * Note however that they may contain name, units and colors information.</li> * <li>The user overrode the {@link Variable#getSampleDimension()} method and returns * {@code null} for whatever raison of his choice.</li> * </ul> * <p> * This method gets the names, colors, ranges of sample values and units for each band * (if available), then delegates the actual work to {@link RenderedSampleDimension}. * * @since 3.20 */ private SampleDimension[] getDefaultSampleDimensions() { Color[][] colors = null; Unit<?>[] units = null; CharSequence[] names = null; final int numBands = this.numBands; for (int i=0; i<numBands; i++) { final Variable variable = variables[i]; if (variable != null) { final Unit<?> unit = variable.getUnit(); if (unit != null) { if (units == null) { units = new Unit<?>[numBands]; } units[i] = unit; } final Color[] c = variable.getColors(); if (c != null) { if (colors == null) { colors = new Color[numBands][]; } colors[i] = c; } final CharSequence name = variable.getName(); if (name != null) { if (names == null) { names = new CharSequence[numBands]; } names[i] = name; } } } final double[] minimum = getArrayProperty("minimum"); final double[] maximum = getArrayProperty("maximum"); if (names != null || minimum != null || maximum != null || units != null || colors != null) { if (raster != null) { return RenderedSampleDimension.create(names, raster, minimum, maximum, units, colors, hints); } else if (image != null) { return RenderedSampleDimension.create(names, image, minimum, maximum, units, colors, hints); } } return null; } /** * Returns the sample dimensions, or {@code null} if none. If all sample dimensions * are actually instances of {@link GridSampleDimension}, then the array type is * {@code GridSampleDimension[]}. * * @return The sample dimensions, or {@code null} if none. * * @since 3.20 */ public SampleDimension[] getSampleDimensions() { SampleDimension[] bands = sampleDimensions; if (bands == null) { // Builds the sample dimension from the variables when first needed. final int numBands = this.numBands; for (int i=numBands; --i>=0;) { final SampleDimension band = variable(i).getSampleDimension(); if (band != null) { if (band instanceof GridSampleDimension) { if (bands == null) { bands = new GridSampleDimension[numBands]; } } else { if (bands == null) { bands = new SampleDimension[numBands]; } else if (bands instanceof GridSampleDimension[]) { final SampleDimension[] old = bands; bands = new SampleDimension[numBands]; System.arraycopy(old, 0, bands, 0, numBands); } } bands[i] = band; } } if (bands == null) { // If the were no variables, build default sample dimensions. bands = getDefaultSampleDimensions(); } else if (bands instanceof GridSampleDimension[]) { final boolean isGeophysics = isGeophysics(); for (int i=0; i<bands.length; i++) { bands[i] = ((GridSampleDimension[]) bands)[i].geophysics(isGeophysics); } } sampleDimensions = bands; } return (bands != null) ? bands.clone() : null; } /** * Sets all sample dimensions. This convenience method {@linkplain #setNumBands(int) * sets the number of bands} to the number of given arguments, then invokes * {@link Variable#setSampleDimension(SampleDimension) setSampleDimension(...)} * for each element. * * @param bands The new sample dimensions, or {@code null}. * * @since 3.20 */ public void setSampleDimensions(final SampleDimension... bands) { setNumBands(bands != null ? bands.length : 0); if (bands != null) { for (int i=0; i<bands.length; i++) { variable(i).setSampleDimension(bands[i]); } } } /** * Sets all variables information to the given ranges, units and colors. This convenience method * {@linkplain #setNumBands(int) sets the number of bands} to the maximal length of the non-null * arrays, then invokes {@link Variable#setSampleRange(NumberRange) setSampleRange(NumberRange)}, * {@link Variable#setUnit(Unit) setUnit(Unit)} and {@link Variable#setColors(Color[]) * setColors(Color[])} methods for each variable. * * @param minValues The minimal value for each band in the raster, or {@code null}. * @param maxValues The maximal value for each band in the raster, or {@code null}. * @param units The units, or {@code null} if unknown. * @param colors The colors to use for values from {@code minValues} to {@code maxValues} * for each bands, or {@code null}. Can contains null elements. * * @since 3.20 */ public void setSampleDimensions(final double[] minValues, final double[] maxValues, final Unit<?> units, final Color[]... colors) { int length = 0; if (minValues != null) length = minValues.length; if (maxValues != null) length = Math.max(length, maxValues.length); if (colors != null) length = Math.max(length, colors.length); for (int i=0; i<length; i++) { double minimum = Double.NEGATIVE_INFINITY; double maximum = Double.POSITIVE_INFINITY; final Variable variable = variable(i); if (minValues != null && i < minValues.length) minimum = minValues[i]; if (maxValues != null && i < maxValues.length) maximum = maxValues[i]; if (minimum != Double.NEGATIVE_INFINITY || maximum != Double.POSITIVE_INFINITY) { variable.setSampleRange(NumberRange.create(minimum, true, maximum, true)); } if (colors != null && i < colors.length) { variable.setColors(colors[i]); } variable.setUnit(units); } } /** * Returns or computes the {@linkplain #image} color model. * This method returns the first non-null value in the above choices, in preference order: * <p> * <ul> * <li>A color model created by one of the {@link GridSampleDimension#getColorModel(int, int) * getColorModel(...)} methods invoked on the first {@link GridSampleDimension} instance * returned by {@linkplain #getSampleDimensions()}.</li> * <li>The {@linkplain #image} color model.</li> * </ul> * * @return The color model, or {@code null} if none. * * @since 3.20 */ public ColorModel getColorModel() { ColorModel cm = null; final SampleDimension[] bands = getSampleDimensions(); if (bands != null) { for (int i=0; i<bands.length; i++) { final SampleDimension band = bands[i]; if (band instanceof GridSampleDimension) { final int dataType = getDataType(); if (dataType != TYPE_UNDEFINED) { cm = ((GridSampleDimension) band).getColorModel(i, bands.length, dataType); } else { cm = ((GridSampleDimension) band).getColorModel(i, bands.length); } if (cm != null) { return cm; } } } } final RenderedImage image = this.image; if (image != null) { cm = image.getColorModel(); } return cm; } /** * Infers the data type from the {@linkplain #image} or the {@linkplain #raster}, if specified. * * @return The data type, or {@code TYPE_UNDEFINED} if unspecified. */ private int getDataType() { SampleModel model = null; if (image != null) { model = image.getSampleModel(); } if (model == null && raster != null) { model = raster.getSampleModel(); } return (model != null) ? model.getDataType() : TYPE_UNDEFINED; } /** * Detects whatever the coverage stores geophysics values or packed values. * This method applies the following criterion, in that order: * <p> * <ul> * <li>If an image has been {@linkplain #setRenderedImage(RenderedImage) explicitely set}, * then this method will infer the status from the image data type.</li> * <li>If at least one {@linkplain SampleDimension sample dimensions} has been * {@linkplain Variable#setSampleDimension(SampleDimension) explicitely set}, * then this method will infer the status from those sample dimensions.</li> * <li>Otherwise this method conservatively returns {@code false}.</li> * </ul> * * @return {@code true} if the coverage stores geophysics values, or {@code false} * if it stores packed values. */ private boolean isGeophysics() { switch (getDataType()) { case TYPE_FLOAT: case TYPE_DOUBLE: return true; case TYPE_BYTE: case TYPE_SHORT: case TYPE_USHORT: case TYPE_INT: return false; } boolean isGeophysics = false; for (int i=numBands; --i>=0;) { final SampleDimension band = variable(i).sampleDimension; if (band != null) { final MathTransform1D sampleToGeophysics = band.getSampleToGeophysics(); if (sampleToGeophysics == null || !sampleToGeophysics.isIdentity()) { return false; } isGeophysics = true; } } return isGeophysics; } /** * Creates a graphics which can be used to draw into the {@linkplain #getRenderedImage() * rendered image}. It is caller responsibility to invoke {@link Graphics#dispose()} after * the drawing has been completed. * <p> * The returned value can usually be casted to {@link Graphics2D}. * * @param crsUnit {@code true} for drawing using the coverage units (typically metres or * degrees of longitude/latitude), or {@code false} for drawing using pixel units. * The value of {@code true} is supported only for {@link Graphics2D} instances. * @return A graphics (usually a {@link Graphics2D}) for drawing into the image. * @throws UnsupportedOperationException If the rendered image does not support drawing, * or if {@code crsUnit} is {@code true} and the graphics is not an instance of * {@link Graphics2D}. * * @since 3.20 */ public Graphics createGraphics(final boolean crsUnit) throws UnsupportedOperationException { RenderedImage image = getRenderedImage(); while (image instanceof RenderedImageAdapter) { image = ((RenderedImageAdapter) image).getWrappedImage(); } final Graphics gr; if (image instanceof Image) { gr = ((Image) image).getGraphics(); } else if (image instanceof PlanarImage) { gr = ((PlanarImage) image).getGraphics(); } else { throw new UnsupportedOperationException(Errors.format(Errors.Keys.UnsupportedImageType)); } if (crsUnit) { final AffineTransform at = getAffineGridToCRS(PixelOrientation.UPPER_LEFT, true); if (at == null) { throw new IllegalStateException(Errors.format(Errors.Keys.UnspecifiedTransform)); } try { ((Graphics2D) gr).transform(at.createInverse()); } catch (NoninvertibleTransformException e) { throw new IllegalArgumentException(Errors.format(Errors.Keys.NoninvertibleTransform), e); } catch (ClassCastException e) { throw new UnsupportedOperationException(Errors.format(Errors.Keys.UnsupportedImageType), e); } } return gr; } /** * Returns the image bounds. * This method returns the first non-null value in the above choices, in preference order: * <p> * <ul> * <li>The {@linkplain #getGridGeometry() grid geometry} extent.</li> * <li>The {@linkplain #image} bounds.</li> * </ul> * <p> * Note that the ({@linkplain Rectangle#x x},{@linkplain Rectangle#y y}) origin must be * (0,0) for building a {@link BufferedImage}, but can be different for other kinds of * {@link RenderedImage}. * * @return The current image bounds. Never {@code null} since no image can be built * without this information. * @throws InvalidGridGeometryException if there is no {@linkplain #getGridGeometry() * grid geometry} or no extent associated to that grid geometry. * * @see GridGeometry2D#getExtent2D() * * @since 3.20 */ public Rectangle getImageBounds() throws InvalidGridGeometryException { final GridGeometry2D gridGeometry = GridGeometry2D.castOrCopy(getGridGeometry()); if (gridGeometry != null) { return gridGeometry.getExtent2D(); } final RenderedImage image = this.image; if (image != null) { return ImageUtilities.getBounds(image); } throw new InvalidGridGeometryException(Errors.Keys.UnspecifiedImageSize); } /** * Returns the tile size, or {@code null} if the image is untiled. * This method returns the first non-null value in the above choices, in preference order: * <p> * <ul> * <li>The value defined by the last call to {@link #setTileSize(Dimension)}.</li> * <li>The {@linkplain #image} tile size.</li> * </ul> * * @return The tile size, or {@code null} if the image is untiled. * * @see RenderedImage#getTileWidth() * @see RenderedImage#getTileHeight() * * @since 3.20 */ public Dimension getTileSize() { final TileLayout tileLayout = this.tileLayout; if (tileLayout != null) { final Dimension size = tileLayout.getSize(); if (size != null) { return size; } } final RenderedImage image = this.image; if (image != null) { final int width = image.getTileWidth(); final int height = image.getTileHeight(); if (width < image.getWidth() || height < image.getHeight()) { return new Dimension(width, height); } } return null; } /** * Sets the tile size. * * {@section Precedence} * If an image has been {@linkplain #setRenderedImage(RenderedImage) explicitely set}, * then its tile setting will have precedence over this attribute for the creation of * {@link GridCoverage2D} instances (i.e. this method does not retile existing images). * * @param size The new tile size, or {@code null} for untiled image. * * @since 3.20 */ public void setTileSize(final Dimension size) { final TileLayout layout = tileLayout; if (layout != null) { layout.setSize(size); } else if (size != null) { tileLayout = new TileLayout(size); } } /** * Returns the tile grid offset, or {@code null} if the image is untiled. * This method returns the first non-null value in the above choices, in preference order: * <p> * <ul> * <li>The value defined by the last call to {@link #setTileGridOffset(Point)}.</li> * <li>The {@linkplain #image} tile grid offset.</li> * </ul> * * @return The tile grid offset, or {@code null} if the image is untiled. * * @see RenderedImage#getTileGridXOffset() * @see RenderedImage#getTileGridYOffset() * * @since 3.20 */ public Point getTileGridOffset() { final TileLayout layout = tileLayout; if (layout != null) { return layout.getLocation(); } final RenderedImage image = this.image; if (image != null && ImageUtilities.isTiled(image)) { return new Point(image.getTileGridXOffset(), image.getTileGridYOffset()); } return null; } /** * Sets the tile offset. * * {@section Precedence} * If an image has been {@linkplain #setRenderedImage(RenderedImage) explicitely set}, * then its tile setting will have precedence over this attribute for the creation of * {@link GridCoverage2D} instances (i.e. this method does not retile existing images). * * @param offset The new tile offset, or {@code null} if none. * * @since 3.20 */ public void setTileGridOffset(final Point offset) { final TileLayout layout = tileLayout; if (layout != null) { layout.setLocation(offset); } else if (offset != null) { tileLayout = new TileLayout(offset); } } /** * Returns the rendered image to be wrapped by {@link GridCoverage2D}. If no image has been * {@linkplain #setRenderedImage(RenderedImage) explicitly defined}, a new one is created the * first time this method is invoked. Users can modify the pixel values in this image before * to create the grid coverage. * <p> * In the common case of untiled image having a {@linkplain GridEnvelope#getLow() lower corner} * located at (0,0), this method returns an instance of {@link BufferedImage}. If the builder * settings do not allow the creation of a {@code BufferedImage} instance, then the default * implementation fallbacks on a {@link TiledImage} instance. * * @return The rendered image to be wrapped by {@code GridCoverage2D}. * * @since 3.20 (derived from 2.5) */ public RenderedImage getRenderedImage() { if (image == null) { final Rectangle bounds = getImageBounds(); // Can not be null. Dimension tileSize = getTileSize(); if (tileSize != null && tileSize.width >= bounds.width && tileSize.height >= bounds.height) { tileSize = null; // Tile size not smaller than image size: untiled image. } Point offset = getTileGridOffset(); if (offset != null && offset.x == bounds.x && offset.y == bounds.y) { offset = null; // Grid offset == image origin: no tile offset. } ColorModel cm = getColorModel(); if (tileSize == null && offset == null && bounds.x == 0 && bounds.y == 0) { if (cm == null) { image = new BufferedImage(bounds.width, bounds.height, BufferedImage.TYPE_BYTE_GRAY); } else { image = new BufferedImage(cm, cm.createCompatibleWritableRaster(bounds.width, bounds.height), false, null); } } else { /* * The code in the above block created a untiled image, which is the most common case. * The code in this block is a fallback used only if the user want to create a tiled * image, or if the origin is not (0,0). We have to use the more generic JAI image * rather than the JDK one. */ if (cm == null) { cm = PlanarImage.getDefaultColorModel(TYPE_BYTE, getNumBands()); } image = new TiledImage(bounds.x, bounds.y, bounds.width, bounds.height, (offset != null) ? offset.x : bounds.x, (offset != null) ? offset.y : bounds.y, cm.createCompatibleSampleModel( (tileSize != null) ? tileSize.width : bounds.width, (tileSize != null) ? tileSize.height : bounds.height), cm); } } return image; } /** * Sets the rendered image. * * @param image The rendered image to be wrapped by {@code GridCoverage2D}, or {@code null}. * * @since 3.20 (derived from 2.5) */ public void setRenderedImage(final RenderedImage image) { this.image = image; raster = null; coverage = null; } /** * Creates a rendered image from the given raster. This methods create a color model * for the given raster, then creates a {@link BufferedImage} using that color model * and the raster, and finally invokes {@link #setRenderedImage(RenderedImage)} with * the result in argument. * * @param raster The raster to be wrapped by {@code GridCoverage2D}, or {@code null}. * * @since 3.20 */ public void setRenderedImage(final WritableRaster raster) { image = null; this.raster = raster; RenderedImage data = null; if (raster != null) { ColorModel cm = getColorModel(); if (cm == null) { cm = PlanarImage.getDefaultColorModel(getDataType(), raster.getNumBands()); } data = new BufferedImage(cm, raster, false, null); } setRenderedImage(data); } /** * Creates a rendered image from the given matrix. This method copies the values from the * given matrix to a new raster, then invokes {@link #setRenderedImage(WritableRaster)}. * {@linkplain Float#NaN NaN} values are mapped to a transparent color by default. * <p> * The {@linkplain RenderedImage#getHeight() image height} will be the length of the * {@code matrix} argument. The {@linkplain RenderedImage#getWidth() image width} will * be the length of the largest row. If some rows or missing ({@code null}) or shorter * than the image width, then the missing values will be padded with {@code NaN}. * * @param matrix The matrix data in a {@code [row][column]} layout. * Can contains {@code null} elements (missing rows). * * @since 3.20 (derived from 2.2) */ public void setRenderedImage(final float[][] matrix) { WritableRaster data = null; if (matrix != null) { int width = 0; int height = matrix.length; for (final float[] row : matrix) { if (row != null) { if (row.length > width) { width = row.length; } } } final float[] buffer = new float[width * height]; int offset = 0; for (final float[] row : matrix) { int length = 0; if (row != null) { length = row.length; System.arraycopy(row, 0, buffer, offset, length); } Arrays.fill(buffer, offset + length, offset += width, Float.NaN); } // Need to use JAI raster factory, since WritableRaster // does not supports TYPE_FLOAT as of J2SE 1.5.0_06. final int[] zero = new int[1]; data = RasterFactory.createBandedRaster( new DataBufferFloat(buffer, buffer.length), width, height, width, zero, zero, null); } setRenderedImage(data); } /** * Creates a rendered image from the given {@linkplain ImageFunction image function}. * The {@link #getGridGeometry()} method must return a fully defined value before * this method can be invoked. * * @param function The image function, or {@code null}. * * @see ImageFunctionDescriptor * * @since 3.20 (derived from 2.2) */ public void setRenderedImage(final ImageFunction function) { RenderedImage data = null; if (function != null) { final AffineTransform at = getAffineGridToCRS(PixelOrientation.CENTER, true); if (at == null) { throw new IllegalStateException(Errors.format(Errors.Keys.UnspecifiedTransform)); } if (at.getShearX()!=0 || at.getShearY()!=0) { // TODO: We may support that in a future version. // 1) Create a copy with shear[X/Y] set to 0. Use the copy. // 2) Compute the residu with createInverse() and concatenate(). // 3) Apply the residu with JAI.create("Affine"). throw new IllegalArgumentException("Shear and rotation not supported"); } final double xScale = at.getScaleX(); final double yScale = at.getScaleY(); final double xTrans = -at.getTranslateX() / xScale; final double yTrans = -at.getTranslateY() / yScale; final GridEnvelope extent = gridGeometry.getExtent(); data = ImageFunctionDescriptor.create( function, extent.getSpan(0), // width extent.getSpan(1), // height (float) xScale, (float) yScale, (float) xTrans, (float) yTrans, hints); } setRenderedImage(data); } /** * Returns the grid coverage. The default implementation builds a coverage like below * (omitting the conversions of some argument types): * * <blockquote><pre>return new {@linkplain GridCoverage2D#GridCoverage2D(CharSequence, * PlanarImage, GridGeometry2D, GridSampleDimension[], GridCoverage[], Map, Hints) GridCoverage2D}( * {@linkplain #getName()}, * {@linkplain #getRenderedImage()}, * {@linkplain #getGridGeometry()}, * {@linkplain #getSampleDimensions()}, * {@linkplain #getSources()}, * {@linkplain #getProperties()}, * {@linkplain #hints})</pre></blockquote> * * @return The grid coverage. */ public GridCoverage2D getGridCoverage2D() { if (coverage == null) { final SampleDimension[] sd = getSampleDimensions(); final GridSampleDimension[] bands; if (sd == null || sd instanceof GridSampleDimension[]) { bands = (GridSampleDimension[]) sd; } else { bands = new GridSampleDimension[sd.length]; for (int i=0; i<bands.length; i++) { bands[i] = GridSampleDimension.castOrCopy(sd[i]); } } coverage = new GridCoverage2D( getName(), getRenderedImage(), GridGeometry2D.castOrCopy(getGridGeometry()), bands, getSources(), getProperties(), hints); } return coverage; } /** * Configure this builder to the same values than the given coverage. * This method invokes the following methods with values inferred from the given coverage: * <p> * <ul> * <li>{@link #setName(CharSequence)} (for instances of {@link GridCoverage2D} only)</li> * <li>{@link #setRenderedImage(RenderedImage)}</li> * <li>{@link #setGridGeometry(GridGeometry)}</li> * <li>{@link #setGridToCRS(MathTransform)}</li> * <li>{@link #setExtent(GridEnvelope)}</li> * <li>{@link #setCoordinateReferenceSystem(CoordinateReferenceSystem)}</li> * <li>{@link #setSampleDimensions(SampleDimension[])}</li> * <li>{@link #setSources(GridCoverage[])}</li> * <li>{@link #setProperties(PropertySource)} (for instances of {@link PropertySource} only)</li> * </ul> * * @param coverage The coverage to set. * * @since 3.20 */ public void setGridCoverage(final GridCoverage coverage) { setCoordinateReferenceSystem(coverage.getCoordinateReferenceSystem()); final GridGeometry gridGeometry = coverage.getGridGeometry(); setGridGeometry(gridGeometry); final SampleDimension[] bands = new SampleDimension[coverage.getNumSampleDimensions()]; for (int i=0; i<bands.length; i++) { bands[i] = coverage.getSampleDimension(i); } setSampleDimensions(bands); final List<GridCoverage> sources = coverage.getSources(); if (sources != null) { setSources(sources.toArray(new GridCoverage[sources.size()])); } if (coverage instanceof PropertySource) { setProperties((PropertySource) coverage); } if (coverage instanceof GridCoverage2D) { final GridCoverage2D c2 = (GridCoverage2D) coverage; setName(c2.getName()); setRenderedImage(c2.getRenderedImage()); this.coverage = c2; // Needs to be last. } else { int gridDimensionX = 0; int gridDimensionY = 1; if (gridGeometry instanceof GridGeometry2D) { final GridGeometry2D g2 = (GridGeometry2D) gridGeometry; gridDimensionX = g2.gridDimensionX; gridDimensionY = g2.gridDimensionY; } final RenderableImage im = coverage.getRenderableImage(gridDimensionX, gridDimensionY); if (im != null) { setRenderedImage(im.createDefaultRendering()); } } /* * Sets explicitely the 'gridToCRS' and extent, then move the grid geometry from "explicit" * state to "cached" state. The intend is to recompute automatically a new grid geometry if * the user change the 'gridToCRS' transform. */ if (gridGeometry != null) { setExtent(gridGeometry.getExtent()); setGridToCRS(gridGeometry.getGridToCRS()); this.gridGeometry = null; cachedGridGeometry = gridGeometry; } } /** * Returns the optional sources to be associated with the coverage. * If there is no sources, then this method returns {@code null}. * * {@section Implementation note} * This method returns a direct reference to the {@linkplain #sources} field. * The array is not cloned because it is not used for any calculation in this * class. {@code GridCoverageBuilder} users are responsible for the content of * this array. * * @return Optional grid coverage sources, or {@code null} if none. * * @see #sources * @see GridCoverage2D#getSources() * * @since 3.20 */ public GridCoverage[] getSources() { return sources; // NOSONAR } /** * Sets the optional sources to be associated with the coverage. * * {@section Implementation note} * The given array is not cloned at this method invocation time. The array will be cloned by * the {@linkplain GridCoverage2D#GridCoverage2D(CharSequence, PlanarImage, GridGeometry2D, * GridSampleDimension[], GridCoverage[], Map, Hints) grid coverage constructor}. This builder * does nothing but passing the array to that constructor. * * @param sources Optional grid coverage sources, or {@code null} or an empty array if none. * * @since 3.20 */ public void setSources(GridCoverage... sources) { if (sources != null && sources.length == 0) { sources = null; } this.sources = sources; // NOSONAR } /** * Returns optional properties to be given to the coverage, or {@code null} if none. * * {@section Implementation note} * This method returns a direct reference to the {@linkplain #properties} field. * The map is not cloned because it is not used for any calculation in this class. * {@code GridCoverageBuilder} users are responsible for the content of this map. * * @return Optional map of coverage properties, or {@code null}. * * @see #properties * @see GridCoverage2D#getProperties() * @see GridCoverage2D#getPropertyNames() * * @since 3.20 */ public Map<?,?> getProperties() { return properties; // NOSONAR } /** * Sets the optional properties to be given to the coverage. * * {@section Reference to the given map} * The given map is not cloned at this method invocation time. The map will be cloned by * the {@linkplain GridCoverage2D#GridCoverage2D(CharSequence, PlanarImage, GridGeometry2D, * GridSampleDimension[], GridCoverage[], Map, Hints) grid coverage constructor}. This builder * does nothing but passing the map to that constructor. * * @param properties Optional map of coverage properties, or {@code null}. * * @since 3.20 */ public void setProperties(final Map<?,?> properties) { this.properties = properties; // NOSONAR } /** * Inherits all the properties from the given source. This convenience methods copies the * properties in a new {@link Map} object, then invokes {@link #setProperties(Map)}. * * @param source The source from which to get the properties, {@code null}. * * @since 3.20 */ public void setProperties(final PropertySource source) { Map<String,Object> map = null; if (source != null) { final String[] names = source.getPropertyNames(); if (names != null && names.length != 0) { map = new LinkedHashMap<>(); for (final String name : names) { map.put(name, source.getProperty(name)); } } } setProperties(map); } /** * Creates the grid coverage. Current implementation delegates to {@link #getGridCoverage2D()}, * but future implementations may instantiate different coverage types. * * @since 3.20 */ @Override public GridCoverage build() { return getGridCoverage2D(); } /** * Resets this builder to its initial state. This method can be invoked in order to * reuse this builder for creating new {@link GridCoverage2D} instances. * * @since 3.20 */ public void reset() { name = null; crs = null; envelope = null; extent = null; gridToCRS = null; pixelAnchor = null; gridGeometry = null; cachedGridGeometry = null; sampleDimensions = null; raster = null; image = null; coverage = null; sources = null; properties = null; numBands = 0; if (variables != null) { Arrays.fill(variables, null); } if (tileLayout != null) { tileLayout.reset(); } } /** * A structure for "no data" value stored in {@link Variable}. */ private static final class NoData { /** The name of the "no data" value, or {@code null}. */ final CharSequence name; /** The color of the "no data" value, or {@code null}. */ final Color color; /** Creates a new structure for the given values. */ NoData(final CharSequence name, final Color color) { this.name = name; this.color = color; } } /** * Helper class for the creation of {@link SampleDimension} instances. * A variable to be mapped to a {@linkplain GridSampleDimension sample dimension}. * * {@note This class is named <cite>variable</cite> because it is usually not needed for * Red/Green/Blue bands. <code>Variable</code> is typically used for describing the * measurement of a single phenomenon, like temperature (°C) or elevation (m). The * <cite>variable</cite> name is used for this purpose in NetCDF files for instance.} * * Variables are obtained by calls to {@link GridCoverageBuilder#variable(int)}. * See {@linkplain GridCoverageBuilder outer class javadoc} for usage examples. * * {@section Subclassing} * Implementors who wish to create their own {@code Variable} subclass will probably * need to override the {@link GridCoverageBuilder#newVariable(int)} method as well. * * @author Martin Desruisseaux (IRD, Geomatys) * @version 3.20 * * @see GridCoverageBuilder#variable(int) * @see ucar.nc2.Variable * * @since 2.5 * @module */ public class Variable extends Builder<SampleDimension> { /** * The band index for this variable. This is the {@code band} argument given * to the {@link GridCoverageBuilder#variable(int)} method. * * @since 3.20 */ protected final int band; /** * The variable name, or {@code null} if unspecified. This field is non-null only if the * name has been {@linkplain #setName(CharSequence) explicitely specified} by the user. * The values inferred from other attributes are not stored in this field. * * @see #getName() * @see #setName(CharSequence) * * @since 3.20 (derived from 2.5) */ protected CharSequence name; /** * The units of measurement, or {@code null} if unspecified. This field is non-null only * if the unit has been {@linkplain #setUnit(Unit) explicitely specified} by the user. * The values inferred from other attributes are not stored in this field. * * @see #getUnit() * @see #setUnit(Unit) * * @since 3.20 (derived from 2.5) */ protected Unit<?> unit; /** * The range of sample values, or {@code null} if unspecified. This field is non-null only * if the range has been {@linkplain #setSampleRange(NumberRange) explicitely specified} * by the user. The values inferred from other attributes are not stored in this field. * * @see #getSampleRange() * @see #setSampleRange(NumberRange) * @see #setSampleRange(int, int) * * @since 3.20 (derived from 2.5) */ protected NumberRange<?> sampleRange; /** * The range of geophysics values, or {@code null} if unspecified. This field is non-null only * if the range has been {@linkplain #setGeophysicsRange(NumberRange) explicitely specified} * by the user. The values inferred from other attributes are not stored in this field. * * @see #getGeophysicsRange() * @see #setGeophysicsRange(NumberRange) * @see #setGeophysicsRange(double, double) * * @since 3.20 */ protected NumberRange<?> geophysicsRange; /** * The "nodata" values, or {@code null} if none. This map is created when first needed. * * @see #addNodataValue(CharSequence, int, Color) */ private Map<Integer,NoData> nodata; /** * The "<cite>sample to unit</cite>" transform, or {@code null} if unspecified. This field * is non-null only if the transform has been {@linkplain #setSampleToUnit(MathTransform1D) * explicitely specified} by the user (potentially as transform coefficients). The values * inferred from other attributes are not stored in this field. * * @see #getSampleToUnit() * @see #setSampleToUnit(MathTransform1D) * @see #setLinearTransform(double, double) * @see #setLogarithmicTransform(double, double) * * @since 3.20 (derived from 2.5) */ protected MathTransform1D sampleToUnit; /** * The colors to associate to values in the {@linkplain #sampleRange sample range}, or * {@code null} if unspecified. * * @since 3.20 */ protected Color[] colors; /** * The sample dimension, or {@code null} if unspecified. This field is non-null only if the * sample dimension has been {@linkplain #setSampleDimension(SampleDimension) explicitely * specified} by the user. The values inferred from other attributes are not stored in this * field. * * @see #getSampleDimension() * @see #setSampleDimension(SampleDimension) * * @since 3.20 (derived from 2.5) */ protected SampleDimension sampleDimension; /** * The sample dimension calculated from other attributes. Will be created when first * needed and cleared when attributes change. * * @since 3.20 */ private transient SampleDimension cached; /** * Creates an initially empty variable. * * @param band The band index for this variable. This is the {@code band} argument given * to the {@link GridCoverageBuilder#variable(int)} method. * * @see GridCoverageBuilder#variable(int) * @see GridCoverageBuilder#newVariable(int) * * @since 3.20 */ protected Variable(final int band) { this.band = band; this.name = ""+band; } /** * Invoked when this sample dimension changed. Note: we keep the plural form in the * method name (despite modifying only this single dimension) for making sure that * we don't invoke by accident the method from the outer class instead than this one. */ private void sampleDimensionsChanged() { cached = null; GridCoverageBuilder.this.sampleDimensionsChanged(); } /** * Returns the name of this variable (or bands or sample dimension). If no name has been * {@linkplain #setName(CharSequence) explicitly defined}, then this method returns the * {@linkplain #sampleDimension sample dimension} description. If this description is not * defined neither, then this method returns the {@linkplain GridCoverageBuilder#name * coverge name}. * * @return The variable name, or {@code null}. * * @see Category#getName() * @see SampleDimension#getDescription() * * @since 3.20 */ public CharSequence getName() { CharSequence value = name; if (value == null) { final SampleDimension sampleDimension = this.sampleDimension; if (sampleDimension != null) { value = sampleDimension.getDescription(); if (value != null) { return value; } } value = GridCoverageBuilder.this.name; } return value; } /** * Sets the name of this variable (or bands or sample dimension). This name will be * given to the geophysics category (if any) and to the sample dimension as a whole. * * @param name The new name, or {@code null}. * * @since 3.20 */ public void setName(final CharSequence name) { this.name = name; sampleDimensionsChanged(); } /** * Returns the units of measurement, or {@code null} if none. If no unit has been * {@linkplain #setUnit(Unit) explicitly defined}, then this method returns the * {@linkplain #sampleDimension sample dimension} unit. * * @return The units of measurement of geophysics values, or {@code null}. * * @see SampleDimension#getUnits() * * @since 3.20 */ public Unit<?> getUnit() { Unit<?> value = unit; if (value == null) { final SampleDimension sampleDimension = this.sampleDimension; if (sampleDimension != null) { value = sampleDimension.getUnits(); } } return value; } /** * Sets the units of measurement, or {@code null} if none. This is the unit of geophysics * values <em>after</em> the {@linkplain #getSampleToUnit() sample to unit transform} has * been applied on sample values. * * @param unit The new units of measurement of geophysics values, or {@code null}. * * @since 3.20 */ public void setUnit(final Unit<?> unit) { this.unit = unit; sampleDimensionsChanged(); } /** * Sets the units of measurement from the given symbol. The default implementation parses * the given symbol using the {@link Units#valueOf(String)} method, then passes the result * to {@link #setUnit(Unit)}. * * @param symbol The new units symbol, or {@code null}. * * @since 3.20 */ public void setUnit(final String symbol) { setUnit(Units.valueOf(symbol)); } /** * Returns the range of sample values. This is the range of values as they are stored * in the image, <em>before</em> the {@linkplain #getSampleToUnit() sample to unit} * transform has been applied. * <p> * This method returns the first defined value in the above choices, in preference order: * <p> * <ul> * <li>The value defined by the last call to {@link #setSampleRange(NumberRange)}.</li> * <li>The {@linkplain #sampleDimension sample dimension} range.</li> * <li>A range built from the {@code "minimum"} and {@code "maximum"} properties of the * {@linkplain GridCoverageBuilder#image image}, if such properties are defined and * are of type {@code double[]}. Those properties can be created by the JAI * "{@link javax.media.jai.operator.ExtremaDescriptor Extrema}" operation.</li> * </ul> * * @return The range of sample values, or {@code null}. * * @since 3.20 (derived from 2.5) */ public NumberRange<?> getSampleRange() { NumberRange<?> range = sampleRange; if (range == null) { final SampleDimension sampleDimension = this.sampleDimension; if (sampleDimension instanceof GridSampleDimension) { range = ((GridSampleDimension) sampleDimension).getRange(); if (range == null) { final double[] minimum = getArrayProperty("minimum"); if (minimum != null && minimum.length > band) { final double[] maximum = getArrayProperty("maximum"); if (maximum != null && maximum.length > band) { range = NumberRange.create(minimum[band], true, maximum[band], true); // TODO: would be nice to cast to the type actually used. } } } } } return range; } /** * Sets the range of sample values. * * {@note We allow only one range of values per <code>Variable</code> instance, because * sample dimensions with multi-ranges of values are very rare. If such case happen, a * <code>GridSampleDimension</code> instance will need to be created from outside this * helper class.} * * @param range The new range of sample values, or {@code null}. * * @since 3.20 (derived from 2.5) */ public void setSampleRange(final NumberRange<?> range) { sampleRange = range; sampleDimensionsChanged(); } /** * Sets the range of sample values from the given lower and upper values. * This convenience methods creates a {@link NumberRange} objects from the * given values and delegates to {@link #setSampleRange(NumberRange)}. * * @param lower The lower sample value (inclusive), typically 0. * @param upper The upper sample value (exclusive), typically 256. * * @since 3.20 (derived from 2.5) */ public void setSampleRange(final int lower, final int upper) { setSampleRange(NumberRange.create(lower, true, upper, false)); } /** * Returns the range of geophysics values. This is the range of values <em>after</em> the * {@linkplain #getSampleToUnit() sample to unit} transform has been applied. * <p> * This method returns the first defined value in the above choices, in preference order: * <p> * <ul> * <li>The value defined by the last call to {@link #setGeophysicsRange(NumberRange)}.</li> * <li>The {@linkplain #sampleDimension sample dimension} geophysics range.</li> * </ul> * * @return The range of geophysics values, or {@code null}. * * @since 3.20 */ public NumberRange<?> getGeophysicsRange() { NumberRange<?> range = geophysicsRange; if (range == null) { final SampleDimension sampleDimension = this.sampleDimension; if (sampleDimension instanceof GridSampleDimension) { range = ((GridSampleDimension) sampleDimension).geophysics(true).getRange(); } } return range; } /** * Sets the range of geophysics values. * * {@note We allow only one range of values per <code>Variable</code> instance, because * sample dimensions with multi-ranges of values are very rare. If such case happen, a * <code>GridSampleDimension</code> instance will need to be created from outside this * helper class.} * * {@section Precedence} * If a <cite>sample to unit</cite> transform has been {@linkplain #setSampleToUnit(MathTransform1D) * explicitely set}, then that later transform will have precedence for the creation of * {@link SampleDimension} instances. * * @param range The new range of geophysics values, or {@code null}. * * @since 3.20 */ public void setGeophysicsRange(final NumberRange<?> range) { geophysicsRange = range; sampleDimensionsChanged(); } /** * Sets the range of geophysics values from the given minimum and maximum values. * This convenience methods creates a {@link NumberRange} objects from the * given values and delegates to {@link #setGeophysicsRange(NumberRange)}. * * @param minimum The minimum value (inclusive). * @param maximum The maximum value (exclusive). * * @since 3.20 */ public void setGeophysicsRange(final double minimum, final double maximum) { setGeophysicsRange(NumberRange.create(minimum, true, maximum, false)); } /** * Returns the "<cite>sample to unit</cite>" transform. * This method returns the first defined value in the above choices, in preference order: * <p> * <ul> * <li>The value defined by the last call to {@link #setSampleToUnit(MathTransform1D)}.</li> * <li>The {@linkplain #sampleDimension sample dimension} transform.</li> * </ul> * * @return The "<cite>sample to unit</cite>" transform, or {@code null}. */ public MathTransform1D getSampleToUnit() { MathTransform1D value = sampleToUnit; if (value == null) { final SampleDimension sampleDimension = this.sampleDimension; if (sampleDimension != null) { value = sampleDimension.getSampleToGeophysics(); } } return value; } /** * Sets the "<cite>sample to unit</cite>" transform. * * @param sampleToUnit The new "<cite>sample to unit</cite>" transform, or {@code null}. */ public void setSampleToUnit(final MathTransform1D sampleToUnit) { this.sampleToUnit = sampleToUnit; sampleDimensionsChanged(); } /** * Sets the "<cite>sample to unit</cite>" transform from a scale and an offset. * The transformation formula will be: * * <blockquote> * <var>geophysics</var> = {@code scale} × <var>sample</var> + {@code offset} * </blockquote> * * @param scale The {@code scale} term in the linear equation. * @param offset The {@code offset} term in the linear equation. */ public void setLinearTransform(final double scale, final double offset) { setSampleToUnit((MathTransform1D) MathTransforms.linear(scale, offset)); } /** * Sets the "<cite>sample to unit</cite>" logarithmic transform * from a scale and an offset. The transformation formula will be: * * <blockquote> * <var>geophysics</var> = log<sub>{@code base}</sub>(<var>sample</var>) + {@code offset} * </blockquote> * * @param base The base of the logarithm (typically 10). * @param offset The offset to add to the logarithm. */ public void setLogarithmicTransform(final double base, final double offset) { final TransferFunction f = new TransferFunction(); f.setType(TransferFunctionType.LOGARITHMIC); f.setBase(base); f.setOffset(offset); setSampleToUnit(f.getTransform()); } /** * Adds a "nodata" value. * * @param name The name for the "nodata" value. * @param value The pixel value to assign to "nodata". * @param color The color to assign to the "nodata" value, or {@code null} for a default color. * @throws IllegalArgumentException if the given pixel value is already assigned. */ public void addNodataValue(final CharSequence name, final int value, final Color color) throws IllegalArgumentException { if (nodata == null) { nodata = new TreeMap<>(); } final Integer key = value; final NoData old = nodata.put(key, new NoData(name, color)); if (old != null) { nodata.put(key, old); throw new IllegalArgumentException(Errors.format( Errors.Keys.ValueAlreadyDefined_1, key)); } sampleDimensionsChanged(); } /** * Returns the colors associated to the values in the {@linkplain #getSampleRange() * sample range}, or {@code null} if none. * * {@section Implementation note} * This method returns a direct reference to the {@linkplain #colors} field. * The array is not cloned because it is not used for any calculation in this * class. {@code GridCoverageBuilder} users are responsible for the content of * this array. * * @return The colors ramp, or {@code null} if none. * * @since 3.20 */ public Color[] getColors() { return colors; // NOSONAR } /** * Sets the colors associated to the values in the {@linkplain #getSampleRange() sample range}. * The given color array can have any length; colors will be interpolated as needed. * * {@section Implementation note} * The given array is not cloned at this method invocation time. The array will be cloned by * the {@linkplain Category#Category(CharSequence, Color[], NumberRange, MathTransform1D) * category constructor}. This builder does nothing but passing the array to that constructor. * * @param colors The new colors ramp (any length), or {@code null} if none. * * @since 3.20 */ public void setColors(Color... colors) { if (colors != null && colors.length == 0) { colors = null; } this.colors = colors; // NOSONAR sampleDimensionsChanged(); } /** * Sets the colors associated to the values in the {@linkplain #getSampleRange() * sample range}. The {@code palette} argument can be any of the * <a href="../../image/io/doc-files/palettes.html">build-in palettes</a>, * or any additional palette defined by the extension mechanism. * * @param palette The name of the new colors ramp. * * @see PaletteFactory#getColors(String) * * @since 3.20 */ public void setColors(final String palette) { try { setColors(PaletteFactory.getDefault().getColors(palette)); } catch (IOException e) { throw new BackingStoreException(e); } } /** * Returns the sample dimension. If no dimension has been * {@linkplain #setSampleDimension(SampleDimension) explicitly defined}, then this method * builds a new dimension from the other attributes defined in this class. * * @return The sample dimension for this {@code Variable} object, or {@code null}. */ public SampleDimension getSampleDimension() { SampleDimension sd = sampleDimension; if (sd == null) { sd = cached; if (sd == null) { Category[] categories; int count = 0; // Number of categories. final CharSequence bandName = getName(); final NumberRange<?> range = getSampleRange(); final Map<Integer,NoData> nodata = this.nodata; /* * Creates the categories for "no data" values, if any. We may keep an empty * slot at the end of the category array for the "quantitative" category. */ if (isNullOrEmpty(nodata)) { categories = (range != null) ? new Category[1] : null; } else { categories = new Category[nodata.size() + (range != null ? 1 : 0)]; for (final Map.Entry<Integer,NoData> entry : nodata.entrySet()) { final int sample = entry.getKey(); if (range != null) { if (range.containsAny(sample)) { throw new IllegalStateException(Errors.format( Errors.Keys.ValueAlreadyDefined_1, sample)); } } final NoData n = entry.getValue(); categories[count++] = new Category(n.name, n.color, sample); } } /* * Creates the quantitative category. We use the "sample to unit" transform * if provided, and the geophysics range as a fallback only if there is no * transform. */ if (range != null) { final Color[] colors = getColors(); MathTransform1D sampleToUnit = getSampleToUnit(); if (sampleToUnit == null) { final NumberRange<?> target = getGeophysicsRange(); if (target == null) { sampleToUnit = (MathTransform1D) MathTransforms.identity(1); } else { // Let the Category constructor create a "sample to unit" transform. categories[count] = new Category(bandName, colors, range, target); } } if (sampleToUnit != null) { categories[count] = new Category(bandName, colors, range, sampleToUnit); } } /* * Creates the sample dimension only if there is at least one attribute set. * Note that the units are ignored if there is not at least one category, so * we don't need to test it. We test the name field (not the local variable) * because the local variable may contain a generated name. */ if (categories != null || this.name != null) { cached = sd = new GridSampleDimension(bandName, categories, getUnit()); } } } return sd; } /** * Sets the sample dimension. If non-null, the given value will have precedence over * all other attributes specified in this class. * * @param dim The new sample dimension, or {@code null}. * * @since 3.20 */ public void setSampleDimension(final SampleDimension dim) { sampleDimension = dim; sampleDimensionsChanged(); } /** * Builds the sample dimension from the parameter defined in this class. * The default implementation delegates to {@link #getSampleDimension()}. * * @return The sample dimension. */ @Override public SampleDimension build() { return getSampleDimension(); } /** * Returns a string representation of this variable. * This string is for debugging purpose only and may change in any future version. */ @Override public String toString() { final StringBuilder buffer = new StringBuilder(getClass().getSimpleName()); buffer.append('['); if (name != null) { buffer.append('"').append(name).append('"'); if (unit != null) { buffer.append(' '); } } if (unit != null) { buffer.append('(').append(unit).append(')'); } return buffer.append(']').toString(); } } }