/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2008, Open Source Geospatial Foundation (OSGeo) * * 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.geotools.coverage.grid; import java.awt.Dimension; import java.awt.image.BufferedImage; import java.awt.image.ColorModel; import java.awt.image.IndexColorModel; import java.awt.image.WritableRaster; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Random; import java.util.TreeMap; import javax.imageio.ImageIO; import javax.measure.unit.Unit; import org.opengis.geometry.Envelope; import org.opengis.referencing.FactoryException; import org.opengis.referencing.operation.MathTransform1D; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.operation.TransformException; import org.geotools.coverage.Category; import org.geotools.coverage.CoverageFactoryFinder; import org.geotools.coverage.GridSampleDimension; import org.geotools.geometry.GeneralEnvelope; import org.geotools.referencing.CRS; import org.geotools.referencing.crs.DefaultGeographicCRS; import org.geotools.referencing.operation.transform.LinearTransform1D; import org.geotools.referencing.operation.transform.LogarithmicTransform1D; import org.geotools.resources.i18n.Errors; import org.geotools.resources.i18n.ErrorKeys; import org.geotools.util.NumberRange; /** * Helper class for the creation of {@link GridCoverage2D} instances. The only purpose of this * builder is to make {@code GridCoverage2D} construction a little bit easier for some common * cases. This class provides default values for each property which make it convenient for simple * cases and testing purpose, but is not generic. Users wanting more control and flexibility should * use {@link GridCoverageFactory} directly. * <p> * Usage example: * * <blockquote><pre> * GridCoverageBuilder builder = new GridCoverageBuilder(); * builder.{@linkplain #setCoordinateReferenceSystem(String) setCoordinateReferenceSystem("EPSG:4326"); * builder.{@linkplain #setEnvelope(double...) setEnvelope}(-60, 40, -50, 50); * * // Will use sample value in the range 0 inclusive to 20000 exclusive. * builder.{@linkplain #setSampleRange(int,int) setSampleRange}(0, 20000); * * // Defines elevation (m) = sample / 10 * Variable elevation = builder.{@linkplain #newVariable newVariable}("Elevation", SI.METRE); * elevation.{@linkplain GridCoverageBuilder.Variable#setLinearTransform setLinearTransform}(0.1, 0); * elevation.addNodataValue("No data", 32767); * * // Gets the image, draw anything we want in it. * builder.{@linkplain #setImageSize(int,int) setImageSize}(500,500); * BufferedImage image = builder.{@linkpalin #getBufferedImage getBufferedImage}(); * Graphics2D gr = image.createGraphics(); * gr.draw(...); * gr.dispose(); * * // Gets the coverage. * GridCoverage2D coverage = builder.{@linkplain #getGridCoverage2D getGridCoverage2D}(); * </pre></blockquote> * * @since 2.5 * @author Martin Desruisseaux * * @source $URL$ * @version $Id$ */ public class GridCoverageBuilder { /** * The envelope, including coordinate reference system. */ private GeneralEnvelope envelope; /** * The range of sample values. */ private NumberRange<? extends Number> range; /** * The default {@linkplain #range}. */ private static final NumberRange<Integer> DEFAULT_RANGE = NumberRange.create(0, true, 256, false); /** * The list of variables created. Each variable will be mapped to a * {@linkplain GridSampleDimension sample dimension}. * * @see #newVariable */ protected final List<Variable> variables; /** * The image size. */ private int width, height; /** * The image. Will be created only when first needed. */ private BufferedImage image; /** * The grid coverage. Will be created only when first needed. */ private GridCoverage2D coverage; /** * The factory to use for creating grid coverages. */ private final GridCoverageFactory factory; /** * Creates a builder initialized to default values and factory. */ public GridCoverageBuilder() { this(CoverageFactoryFinder.getGridCoverageFactory(null)); } /** * Creates a builder initialized to default values. */ public GridCoverageBuilder(final GridCoverageFactory factory) { this.factory = factory; variables = new ArrayList<Variable>(); width = 256; height = 256; } /** * Wraps an arbitrary envelope to an object that can be stored in {@link #envelope}. */ private static GeneralEnvelope wrap(final Envelope envelope) { return (envelope==null || envelope instanceof GeneralEnvelope) ? (GeneralEnvelope) envelope : new GeneralEnvelope(envelope); } /** * Returns the current coordinate reference system. If no CRS has been * {@linkplain #setCoordinateReferenceSystem explicitly defined}, then * the default CRS is {@linkplain DefaultGeographicCRS#WGS84 WGS84}. */ public CoordinateReferenceSystem getCoordinateReferenceSystem() { return (envelope != null) ? envelope.getCoordinateReferenceSystem() : DefaultGeographicCRS.WGS84; } /** * Sets the coordinate reference system to the specified value. If an * {@linkplain #setEnvelope envelope was previously defined}, it will * be reprojected to the new CRS. * * @throws IllegalArgumentException if the CRS is illegal for the * {@linkplain #getEnvelope current envelope}. */ public void setCoordinateReferenceSystem(final CoordinateReferenceSystem crs) throws IllegalArgumentException { if (envelope == null) { if (crs != null) { envelope = wrap(CRS.getEnvelope(crs)); if (envelope == null) { envelope = new GeneralEnvelope(crs); envelope.setToNull(); } } } else try { envelope = wrap(CRS.transform(envelope, crs)); } catch (TransformException exception) { throw new IllegalArgumentException(Errors.format( ErrorKeys.ILLEGAL_COORDINATE_REFERENCE_SYSTEM), exception); } coverage = null; } /** * Sets the coordinate reference system to the specified authority code. This convenience * method gives a preference to axis in (<var>longitude</var>, <var>latitude</var>) order. * * @throws IllegalArgumentException if the given CRS is illegal. */ public void setCoordinateReferenceSystem(final String code) throws IllegalArgumentException { final CoordinateReferenceSystem crs; try { crs = CRS.decode(code, true); } catch (FactoryException exception) { throw new IllegalArgumentException(Errors.format( ErrorKeys.ILLEGAL_COORDINATE_REFERENCE_SYSTEM), exception); } setCoordinateReferenceSystem(crs); } /** * Returns a copy of current envelope. If no envelope has been {@linkplain #setEnvelope * explicitly defined}, then the default is inferred from the CRS (by default a geographic * envelope from 180°W to 180°E and 90°S to 90°N). */ public Envelope getEnvelope() { if (envelope != null) { return envelope.clone(); } else { final CoordinateReferenceSystem crs = getCoordinateReferenceSystem(); Envelope candidate = CRS.getEnvelope(crs); if (candidate == null) { final GeneralEnvelope copy = new GeneralEnvelope(crs); copy.setToNull(); candidate = copy; } return candidate; } } /** * Sets the envelope to the specified value. If a {@linkplain #setCoordinateReferenceSystem * CRS was previously defined}, the envelope will be reprojected to that CRS. If no CRS was * previously defined, then the CRS will be set to the * {@linkplain Envelope#getCoordinateReferenceSystem envelope CRS}. * * @throws IllegalArgumentException if the envelope is illegal for the * {@linkplain #getCoordinateReferenceSystem current CRS}. */ public void setEnvelope(Envelope envelope) throws IllegalArgumentException { if (this.envelope != null) try { envelope = CRS.transform(envelope, this.envelope.getCoordinateReferenceSystem()); } catch (TransformException exception) { throw new IllegalArgumentException(Errors.format( ErrorKeys.ILLEGAL_COORDINATE_REFERENCE_SYSTEM), exception); } this.envelope = new GeneralEnvelope(envelope); coverage = null; } /** * 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. * <p> * <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>) */ public void setEnvelope(final double... ordinates) throws IllegalArgumentException { GeneralEnvelope envelope = this.envelope; if (envelope == null) { envelope = new GeneralEnvelope(ordinates.length / 2); } envelope.setEnvelope(ordinates); this.envelope = envelope; // Assigns only if successful. } /** * Returns the range of sample values. If no range has been {@linkplain #setSampleRange * explicitly defined}, then the default is a range from 0 inclusive to 256 exclusive. */ public NumberRange<? extends Number> getSampleRange() { return (range != null) ? range : DEFAULT_RANGE; } /** * Sets the range of sample values. */ public void setSampleRange(final NumberRange<? extends Number> range) { this.range = range; coverage = null; } /** * Sets the range of sample values. * * @param lower The lower sample value (inclusive), typically 0. * @param upper The upper sample value (exclusive), typically 256. */ public void setSampleRange(final int lower, final int upper) { setSampleRange(NumberRange.create(lower, true, upper, false)); } /** * Returns the image size. If no size has been {@linkplain #setImageSize explicitly defined}, * then the default is 256×256 pixels. */ public Dimension getImageSize() { return new Dimension(width, height); } /** * Sets the image size. */ public void setImageSize(final Dimension size) { width = size.width; height = size.height; image = null; coverage = null; } /** * Sets the image size. */ public void setImageSize(final int width, final int height) { setImageSize(new Dimension(width, height)); } /** * Creates a new variable, which will be mapped to a {@linkplain GridSampleDimension sample * dimension}. Additional information like scale, offset and nodata values can be provided * by invoking setters on the returned variable. * * @param name The variable name, or {@code null} for a default name. * @param units The variable units, or {@code null} if unknown. * @return A new variable. */ public Variable newVariable(final CharSequence name, final Unit<?> units) { final Variable variable = new Variable(name, units); variables.add(variable); return variable; } /** * Returns the buffered image to be wrapped by {@link GridCoverage2D}. If no image has been * {@linkplain #setBufferedImage explicitly defined}, a new one is created the first time * this method is invoked. Users can write in this image before to create the grid coverage. */ public BufferedImage getBufferedImage() { if (image == null) { final int numBands = variables.size(); if (numBands == 0) { image = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY); } else { final GridSampleDimension sd = variables.get(0).getSampleDimension(); final ColorModel cm; if (numBands == 1) { cm = sd.getColorModel(); } else { cm = sd.getColorModel(0, numBands); } final WritableRaster raster = cm.createCompatibleWritableRaster(width, height); image = new BufferedImage(cm, raster, false, null); } } return image; } /** * Sets the buffered image. Invoking this method overwrite the * {@linkplain #getImageSize image size} with the given image size. */ public void setBufferedImage(final BufferedImage image) { setImageSize(image.getWidth(), image.getHeight()); this.image = image; // Stores only if the above line succeed. coverage = null; } /** * Sets the buffered image by reading it from the given file. Invoking this method * overwrite the {@linkplain #getImageSize image size} with the given image size. * * @throws IOException if the image can't be read. */ public void setBufferedImage(final File file) throws IOException { setBufferedImage(ImageIO.read(file)); } /** * Sets the buffered image to a raster filled with random value using the specified random * number generator. This method can be used for testing purpose, or for adding noise to a * coverage. */ public void setBufferedImage(final Random random) { image = null; // Will forces the creation of a new BufferedImage. final BufferedImage image = getBufferedImage(); final WritableRaster raster = image.getRaster(); final ColorModel model = image.getColorModel(); final int size; if (model instanceof IndexColorModel) { size = ((IndexColorModel) model).getMapSize(); } else { size = 1 << Short.SIZE; } for (int i=raster.getWidth(); --i>=0;) { for (int j=raster.getHeight(); --j>=0;) { raster.setSample(i,j,0, random.nextInt(size)); } } } /** * Returns the grid coverage. */ public GridCoverage2D getGridCoverage2D() { if (coverage == null) { final BufferedImage image = getBufferedImage(); final Envelope envelope = getEnvelope(); final GridSampleDimension[] bands; if (variables.isEmpty()) { bands = null; } else { bands = new GridSampleDimension[variables.size()]; for (int i=0; i<bands.length; i++) { bands[i] = variables.get(i).getSampleDimension(); } } coverage = factory.create(null, image, envelope, bands, null, null); } return coverage; } /** * A variable to be mapped to a {@linkplain GridSampleDimension sample dimension}. * Variables are created by {@link GridCoverageBuilder#newVariable}. * * @since 2.5 * @author Martin Desruisseaux * @source $URL$ * @version $Id$ */ public class Variable { /** * The variable name, or {@code null} for a default name. */ private final CharSequence name; /** * The variable units, or {@code null} for a default units. */ private final Unit<?> units; /** * The "nodata" values. */ private final Map<Integer,CharSequence> nodata; /** * The "<cite>sample to geophysics</cite>" transform. */ private MathTransform1D transform; /** * The sample dimension. Will be created when first needed. May be reset to {@code null} * after creation if a new sample dimension need to be computed. */ private GridSampleDimension sampleDimension; /** * Creates a new variable of the given name and units. * * @param name The variable name, or {@code null} for a default name. * @param units The variable units, or {@code null} if unknown. * * @see GridCoverageBuilder#newVariable */ protected Variable(final CharSequence name, final Unit<?> units) { this.name = name; this.units = units; this.nodata = new TreeMap<Integer,CharSequence>(); } /** * Returns the "<cite>sample to geophysics</cite>" transform, or {@code null} if none. */ public MathTransform1D getTransform() { return transform; } /** * Sets the "<cite>sample to geophysics</cite>" transform. */ public void setTransform(final MathTransform1D transform) { this.transform = transform; sampleDimension = null; } /** * Sets the "<cite>sample to geophysics</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) { setTransform(LinearTransform1D.create(scale, offset)); } /** * Sets the "<cite>sample to geophysics</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) { setTransform(LogarithmicTransform1D.create(base, offset)); } /** * Adds a "nodata" value. * * @param name The name for the "nodata" value. * @param value The pixel value to assign to "nodata". * @throws IllegalArgumentException if the given pixel value is already assigned. */ public void addNodataValue(final CharSequence name, final int value) throws IllegalArgumentException { final Integer key = value; final CharSequence old = nodata.put(key, name); if (old != null) { nodata.put(key, old); throw new IllegalArgumentException(Errors.format( ErrorKeys.ILLEGAL_ARGUMENT_$2, "value", key)); } sampleDimension = null; } /** * Returns a sample dimension for the current * {@linkplain GridCoverageBuilder#getSampleRange range of sample values}. */ public GridSampleDimension getSampleDimension() { if (sampleDimension == null) { NumberRange<? extends Number> range = getSampleRange(); int lower = (int) Math.floor(range.getMinimum(true)); int upper = (int) Math.ceil(range.getMaximum(false)); final Category[] categories = new Category[nodata.size() + 1]; int i = 0; for (final Map.Entry<Integer,CharSequence> entry : nodata.entrySet()) { final int sample = entry.getKey(); if (sample >= lower && sample < upper) { if (sample - lower <= upper - sample) { lower = sample + 1; } else { upper = sample; } } categories[i++] = new Category(entry.getValue(), null, sample); } range = NumberRange.create(lower, true, upper, false); categories[i] = new Category(name, null, range, (transform != null) ? transform : LinearTransform1D.IDENTITY); sampleDimension = new GridSampleDimension(name, categories, units); } return sampleDimension; } /** * Returns a string representation of this variable. */ @Override public String toString() { final StringBuilder buffer = new StringBuilder(getClass().getSimpleName()); buffer.append('['); if (name != null) { buffer.append('"').append(name).append('"'); if (units != null) { buffer.append(' '); } } if (units != null) { buffer.append('(').append(units).append(')'); } return buffer.append(']').toString(); } } }