/* * Geotoolkit.org - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2010-2012, Open Source Geospatial Foundation (OSGeo) * (C) 2010-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.io; import java.util.Arrays; import java.util.Locale; import java.util.Iterator; import java.util.Collections; import java.util.logging.Level; import java.util.concurrent.CancellationException; import java.awt.Rectangle; import java.awt.Dimension; import java.awt.RenderingHints; import java.awt.image.RenderedImage; import java.io.IOException; import javax.imageio.ImageIO; import javax.imageio.IIOImage; import javax.imageio.ImageWriter; import javax.imageio.ImageWriteParam; import javax.imageio.ImageTypeSpecifier; import javax.imageio.spi.ImageWriterSpi; import javax.imageio.metadata.IIOMetadata; import javax.imageio.stream.ImageOutputStream; import javax.media.jai.JAI; import javax.media.jai.Warp; import javax.media.jai.PlanarImage; import javax.media.jai.ImageLayout; import javax.media.jai.Interpolation; import javax.media.jai.RenderedImageAdapter; import javax.media.jai.operator.WarpDescriptor; import org.opengis.util.InternationalString; import org.opengis.geometry.Envelope; import org.opengis.coverage.SampleDimension; import org.opengis.coverage.grid.GridCoverage; import org.opengis.coverage.InterpolationMethod; import org.opengis.metadata.spatial.PixelOrientation; import org.opengis.referencing.operation.MathTransform2D; import org.opengis.referencing.operation.TransformException; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.apache.sis.util.ArraysExt; import org.geotoolkit.io.TableWriter; import org.geotoolkit.image.io.XImageIO; import org.geotoolkit.image.io.MultidimensionalImageStore; import org.geotoolkit.image.io.mosaic.MosaicImageWriter; import org.geotoolkit.image.io.metadata.ReferencingBuilder; import org.geotoolkit.referencing.operation.transform.WarpFactory; import org.apache.sis.referencing.operation.transform.LinearTransform; import org.geotoolkit.coverage.grid.GridGeometry2D; import org.geotoolkit.coverage.AbstractCoverage; import org.geotoolkit.internal.coverage.CoverageUtilities; import org.geotoolkit.internal.image.io.DimensionAccessor; import org.geotoolkit.internal.image.io.GridDomainAccessor; import org.geotoolkit.nio.IOUtilities; import org.geotoolkit.resources.Errors; import static org.geotoolkit.image.io.MultidimensionalImageStore.*; import static org.geotoolkit.image.io.metadata.SpatialMetadataFormat.GEOTK_FORMAT_NAME; /** * A {@link GridCoverageWriter} implementation which use an {@link ImageWriter} for writing * sample values. This implementation reads the sample values from a {@link RenderedImage}, * and consequently is targeted toward two-dimensional slices of data. * <p> * {@code ImageCoverageWriter} basically works as a layer which converts <cite>geodetic * coordinates</cite> (for example the region to read) to <cite>pixel coordinates</cite> * before to pass them to the wrapped {@code ImageWriter}, and conversely: from pixel * coordinates to geodetic coordinates. The later conversion is called "<cite>grid to CRS</cite>" * and is determined from the {@link GridGeometry2D} provided by the {@link GridCoverage}. * * {@section Closing the output stream} * An {@linkplain ImageOutputStream Image Output Stream} may be created automatically from various * output types like {@linkplain java.io.File} or {@linkplain java.net.URL}. That output stream is * <strong>not</strong> closed after a write operation, because many consecutive write operations * may be performed for the same output. To ensure that the automatically generated output stream * is closed, user shall invoke the {@link #setOutput(Object)} method with a {@code null} input, * or invoke the {@link #reset()} or {@link #dispose()} methods. * <p> * Note that output streams explicitly given by the users are never closed. It is caller * responsibility to close them. * * @author Martin Desruisseaux (Geomatys) * @author Johann Sorel (Geomatys) * @version 3.20 * * @since 3.14 * @module */ public class ImageCoverageWriter extends GridCoverageWriter { /** * The {@link ImageWriter} to use for encoding {@link RenderedImage}s. This writer is initially * {@code null} and lazily created when first needed. Once created, it is reused for subsequent * outputs if possible. */ protected ImageWriter imageWriter; /** * Creates a new instance. */ public ImageCoverageWriter() { } /** * Sets the logging level to use for write operations. If the {@linkplain #imageWriter image * writer} implements the {@link org.geotoolkit.util.logging.LogProducer} interface, then it * is also set to the given level. * * @since 3.15 */ @Override public void setLogLevel(final Level level) { super.setLogLevel(level); copyLevel(imageWriter); } /** * {@inheritDoc} * <p> * The given locale will also be given to the wrapped {@linkplain #imageWriter image writer}, * providing that the image writer supports the locale language. If it doesn't, then the image * writer locale is set to {@code null}. */ @Override public void setLocale(final Locale locale) { super.setLocale(locale); setLocale(imageWriter, locale); } /** * Sets the given locale to the given {@link ImageWriter}, provided that the image writer * supports the language of that locale. Otherwise sets the writer locale to {@code null}. * * @see ImageReader#setLocale(Locale) */ private static void setLocale(final ImageWriter writer, final Locale locale) { if (writer != null) { writer.setLocale(select(locale, writer.getAvailableLocales())); } } /** * Sets the output destination to the given object. The output is typically a * {@link java.io.File}, {@link java.net.URL} or {@link String} object, but other * types (especially {@link ImageOutputStream}) may be accepted as well depending * on the {@linkplain #imageWriter image writer} implementation. * <p> * The given output can also be an {@link ImageWriter} instance with its output initialized, * in which case it is used directly as the {@linkplain #imageWriter image writer} wrapped * by this {@code ImageCoverageWriter}. * * @param output The output (typically {@link java.io.File} or {@link String}) to be written. * @throws IllegalArgumentException if output is not a valid instance for this writer. * @throws CoverageStoreException if the operation failed. */ @Override public void setOutput(final Object output) throws CoverageStoreException { try { close(); } catch (IOException e) { throw new CoverageStoreException(formatErrorMessage(output, e, true), e); } super.setOutput(output); } /** * Sets the image writer output. This method ensures that the {@link #imageWriter} field is * set to a suitable {@link ImageWriter} instance. This is done by invoking the following * methods, which can be overridden by subclasses: * <p> * <ol> * <li>If the current {@link #imageWriter} is non-null, invoke * {@link #canReuseImageWriter(ImageWriterSpi, String, Object, RenderedImage)} * for determining if it can be reused for the new output.</li> * <li>If the current {@code imageWriter} was null or if the above method call * returned {@code false}, invoke {@link #createImageWriter(String, Object, RenderedImage)} * for creating a new {@link ImageWriter} instance for the given output.</li> * </ol> * <p> * Then this method {@linkplain ImageWriter#setOutput(Object) sets the output} of the * {@link #imageWriter} instance, if it was not already done by the above method calls. * * @param output The output (typically {@link java.io.File} or {@link String}) to be written. * @param image The image to be written, or {@code null} if unknown. * @param formatName The format to use for fetching an {@link ImageWriter}, or {@code null} * if unspecified. * @throws IllegalArgumentException if output is not a valid instance for this writer. * @throws CoverageStoreException if the operation failed. */ private void setImageOutput(final RenderedImage image, final String formatName) throws CoverageStoreException { final Object output = this.output; if (output != null) try { final ImageWriter oldWriter = imageWriter; ImageWriter newWriter = null; if (output instanceof ImageWriter) { newWriter = (ImageWriter) output; // The old writer will be disposed and the locale will be set below. } else { /* * First, check if the current writer can be reused. If the user * didn't overridden the canReuseImageWriter(...) method, then the * default implementation is to look at the file extension. */ if (oldWriter != null) { final ImageWriterSpi provider = oldWriter.getOriginatingProvider(); if (provider != null && canReuseImageWriter(provider, formatName, output, image)) { newWriter = oldWriter; } } /* * If we can't reuse the old writer, create a new one. If the user didn't * overridden the createImageWriter(...) method, then the default behavior * is to get an image writer by the extension. */ if (newWriter == null) { newWriter = createImageWriter(formatName, output, image); } /* * Set the output if it was not already done. In the default implementation, * this is done by 'createImageWriter' but not by 'canReuseImageWriter'. * However the user could have overridden the above-cited methods with a * different behavior. */ if (newWriter != output && newWriter.getOutput() == null) { Object imageOutput = output; final ImageWriterSpi provider = newWriter.getOriginatingProvider(); if (provider != null) { boolean needStream = false; for (final Class<?> outputType : provider.getOutputTypes()) { if (outputType.isInstance(imageOutput)) { needStream = false; break; } if (outputType.isAssignableFrom(ImageOutputStream.class)) { needStream = true; // Do not break - maybe the output type is accepted later. } } if (needStream) { imageOutput = ImageIO.createImageOutputStream(output); if (imageOutput == null) { final short messageKey; final Object argument; if (IOUtilities.canProcessAsPath(output)) { messageKey = Errors.Keys.CantWriteFile_1; argument = IOUtilities.filename(output); } else { messageKey = Errors.Keys.UnknownType_1; argument = output.getClass(); } throw new CoverageStoreException(Errors.getResources(locale).getString(messageKey, argument)); } } } newWriter.setOutput(imageOutput); } } /* * If the writer has changed, close the output of the old writer, unless the new * writer is using the same output. Note that if the output stream was explicitly * given by the user, then the new writer should have the same output (consequently * it will not be closed). */ if (newWriter != oldWriter) { if (oldWriter != null) { final Object oldOutput = oldWriter.getOutput(); oldWriter.dispose(); if (oldOutput != newWriter.getOutput()) { IOUtilities.close(oldOutput); } } copyLevel(newWriter); setLocale(newWriter, locale); if (LOGGER.isLoggable(getFineLevel())) { ImageCoverageStore.logCodecCreation(this, ImageCoverageWriter.class, newWriter, newWriter.getOriginatingProvider()); } } imageWriter = newWriter; } catch (IOException e) { throw new CoverageStoreException(formatErrorMessage(output, e, true), e); } } /** * Returns {@code true} if the image writer created by the given provider can * be reused. This method is invoked automatically for determining if the current * {@linkplain #imageWriter image writer} can be reused for writing the given output. * <p> * The default implementation performs the following checks: * <p> * <ol> * <li>If {@code formatName} is non-null, then this method checks if the given name is * one of the {@linkplain ImageWriterSpi#getFormatNames() format names declared by * the provider}. If not, then this method immediately returns {@code false}.</li> * * <li>Next, this method returns {@code true} if the writer * {@linkplain ImageWriterSpi#canEncodeImage(RenderedImage) can encode} the given image, * or {@code false} otherwise.</li> * </ol> * * {@section Overriding} * Subclasses can override this method if they want to determine in another way whatever * the {@linkplain #imageWriter image writer} can be reused. Subclasses can optionally * {@linkplain ImageWriter#setOutput(Object) set the image writer output} or leave it * {@code null}, at their choice. If they set the output, then that output will be used. * Otherwise the caller will set the output automatically. * * @param provider The provider of the image writer. * @param formatName The format to use for fetching an {@link ImageWriter}, * or {@code null} if unspecified. * @param output The output to set to the image writer. * @param image The image to be written, or {@code null} if unknown. * @return {@code true} if the image writer can be reused. * @throws IOException If an error occurred while determining if the current * image writer can write the given image to the given output. */ protected boolean canReuseImageWriter(final ImageWriterSpi provider, final String formatName, final Object output, final RenderedImage image) throws IOException { if (formatName != null) { final String[] formats = provider.getFormatNames(); if (formats == null || !ArraysExt.containsIgnoreCase(formats, formatName)) { return false; } } return provider.canEncodeImage(image); } /** * Creates an {@link ImageWriter} that claim to be able to encode the given output. This method * is invoked automatically for assigning a new value to the {@link #imageWriter} field. * <p> * The default implementation performs the following choice: * <p> * <ul> * <li>If a non-null format name is specified, delegate to * {@link XImageIO#getWriterByFormatName(String, Object, RenderedImage)}.</li> * <li>Otherwise delegate to {@link XImageIO#getWriterBySuffix(Object, RenderedImage)}.</li> * </ul> * * {@section Overriding} * Subclasses can override this method if they want to create a new image writer in another way. * Subclasses can optionally {@linkplain ImageWriter#setOutput(Object) set the image writer output} * or leave it {@code null}, at their choice. If they set the output, then that output will be used. * Otherwise the caller will set the output automatically. * * @param formatName The format to use for fetching an {@link ImageWriter}, * or {@code null} if unspecified. * @param output The output destination. * @param image The image to be written, or {@code null} if unknown. * @return An initialized image writer for writing to the given output. * @throws IOException If no suitable image writer has been found, or if an error occurred * while creating it. */ protected ImageWriter createImageWriter(final String formatName, final Object output, final RenderedImage image) throws IOException { if (MosaicImageWriter.Spi.DEFAULT.canEncodeOutput(output)) { return MosaicImageWriter.Spi.DEFAULT.createWriterInstance(); } if (formatName != null) { return XImageIO.getWriterByFormatName(formatName, output, image); } else { return XImageIO.getWriterBySuffix(output, image); } } /** * Returns the default Java I/O parameters to use for writing an image. This method * is invoked by the {@link #write(GridCoverage, GridCoverageWriteParam)} method in * order to get the Java parameter object to use for controlling the writing process. * <p> * The default implementation returns {@link ImageWriter#getDefaultWriteParam()} with * tiling, progressive mode and compression set to {@link ImageWriteParam#MODE_DEFAULT}. * Subclasses can override this method in order to perform additional parameter * settings. Note however that any * {@linkplain ImageWriteParam#setSourceRegion source region}, * {@linkplain ImageWriteParam#setSourceSubsampling source subsampling} and * {@linkplain ImageWriteParam#setSourceBands source bands} settings may be overwritten * by the {@code write} method, which perform its own computation. * * @param image The image which will be written. * @return A default Java I/O parameters object to use for controlling the writing process. * @throws IOException If an I/O operation was required and failed. * * @see #write(GridCoverage, GridCoverageWriteParam) */ protected ImageWriteParam createImageWriteParam(RenderedImage image) throws IOException { final ImageWriteParam param = imageWriter.getDefaultWriteParam(); if (param.canWriteTiles()) { param.setTilingMode(ImageWriteParam.MODE_DEFAULT); } if (param.canWriteProgressive()) { param.setProgressiveMode(ImageWriteParam.MODE_DEFAULT); } if (param.canWriteCompressed()) { param.setCompressionMode(ImageWriteParam.MODE_DEFAULT); } return param; } /** * Creates additional metadata to be merged with the one created by {@code ImageCoverageReader}. * This method is invoked automatically just before to write the image, with a {@code metadata} * argument containing basic information in the {@code RectifiedGridDomain} and {@code Dimensions} * nodes (see <a href="../../image/io/metadata/SpatialMetadataFormat.html#default-formats">Image * metadata</a> for a tree description). The default implementation does nothing. However * subclasses can override this method in order to create additional metadata that this * writer can not infer. * * @param metadata The default metadata, to be modified in-place. * @param coverage {@code null} for {@linkplain ImageWriter#getDefaultStreamMetadata stream metadata}, * or the coverage being written for {@linkplain ImageWriter#getDefaultImageMetadata image metadata}. * @throws IOException If an I/O operation was required and failed. * * @since 3.17 */ protected void completeImageMetadata(IIOMetadata metadata, GridCoverage coverage) throws IOException { } /** * Writes a single grid coverage using {@link ImageWriter#write(IIOMetadata, IIOImage, ImageWriteParam)}. * The default implementation wraps the given coverage in a {@linkplain Collections#singleton(Object) * singleton set} and delegates to {@link #write(Iterable, GridCoverageWriteParam)}. */ @Override public void write(final GridCoverage coverage, final GridCoverageWriteParam param) throws CoverageStoreException, CancellationException { write(Collections.singleton(coverage), param); } /** * Writes a single or many grid coverages using {@link ImageWriter#write(IIOMetadata, IIOImage, * ImageWriteParam) ImageWriter.write} or {@link ImageWriter#writeToSequence(IIOImage, ImageWriteParam) * writeToSequence(IIOImage, ImageWriteParam)}. For each coverage in the given iterable, this * method performs the following steps: * <p> * <ul> * <li>Get the coverage {@link RenderedImage} and {@linkplain GridGeometry2D}.</li> * <li>Create an initially empty block of image parameters by * invoking the {@link #createImageWriteParam(RenderedImage)} method.</li> * <li>Convert the given {@linkplain GridCoverageWriteParam geodetic parameters} to * {@linkplain ImageWriteParam image parameters} using the above grid geometry. * The main properties to be set are: * <ul> * <li>{@linkplain ImageWriteParam#setSourceRegion source region}</li> * <li>{@linkplain ImageWriteParam#setSourceSubsampling source subsampling}</li> * <li>{@linkplain ImageWriteParam#setSourceBands source bands}</li> * </ul></li> * <li>Write the rendered image by invoking the image writer {@code write} or * {@code writeSequence} method, depending on whatever the given iterable * contains one or more coverages.</li> * </ul> * * @see ImageWriter#write(IIOMetadata, IIOImage, ImageWriteParam) * @see ImageWriter#writeToSequence(IIOImage, ImageWriteParam) */ @Override public void write(final Iterable<? extends GridCoverage> coverages, final GridCoverageWriteParam param) throws CoverageStoreException, CancellationException { abortRequested = false; final long startTime = isLoggable() ? System.nanoTime() : Long.MIN_VALUE; final Iterator<? extends GridCoverage> it = coverages.iterator(); if (!it.hasNext()) { throw new CoverageStoreException(Errors.format(Errors.Keys.NoSuchElement_1, GridCoverage.class)); } boolean hasNext; write(it.next(), param, true, !(hasNext = it.hasNext()), startTime); while (hasNext) { // Happen only if writing many coverages in a sequence. write(it.next(), param, false, !(hasNext = it.hasNext()), startTime); } } /** * Writes a single coverage, which may be an element of a sequence. This method needs to be * informed when it is writing the first or the last coverage of a sequence. If there is only * one coverage to write, than both {@code isFirst} and {@code isLast} must be {@code true}. * <p> * In current implementation, the stream metadata are generated from the first image only * (when {@code isFirst == true}) and the log message (if any) shows the grid geometry of * the last coverage only (when {@code isLast == true}). It should not be an issue in the * common case where all coverage in the sequence have similar grid geometry or metadata. * * @param coverages The coverages to write. * @param param Optional parameters used to control the writing process, or {@code null}. * @param isFirst {@code true} if writing the first coverage of a sequence. * @param isLast {@code true} if writing the last coverage of a sequence. * @param startTime Nano time when the writing process started, or {@link Long#MIN_VALUE} * if the operation duration is not logged. * @throws IllegalStateException If the output destination has not been set. * @throws CoverageStoreException If the iterable contains an unsupported number of coverages, * or if an error occurs while writing the information to the output destination. * @throws CancellationException If {@link #abort()} has been invoked in an other thread during * the execution of this method. * * @since 3.20 */ private void write(final GridCoverage coverage, final GridCoverageWriteParam param, final boolean isFirst, final boolean isLast, final long startTime) throws CoverageStoreException, CancellationException { /* * Prepares an initially empty ImageWriteParam, to be filled later with the values * provided in the GridCoverageWriteParam. In order to get the ImageWriteParam, we * need the ImageWriter, which need the RenderedImage, which need the GridGeometry. */ GridGeometry2D gridGeometry = GridGeometry2D.castOrCopy(coverage.getGridGeometry()); RenderedImage image = coverage.getRenderableImage(gridGeometry.gridDimensionX, gridGeometry.gridDimensionY).createDefaultRendering(); while (image instanceof RenderedImageAdapter) { image = ((RenderedImageAdapter) image).getWrappedImage(); } if (isFirst) { final String imageFormat = (param != null) ? param.getFormatName() : null; setImageOutput(image, imageFormat); } /* * The ImageWriter is created by the call to setImageOutput. * We can verify its validity only at this point. */ final ImageWriter imageWriter = this.imageWriter; // Protect from changes. if (imageWriter == null) { throw new IllegalStateException(formatErrorMessage(Errors.Keys.NoImageOutput)); } if (!isLast && !imageWriter.canWriteSequence()) { throw new CoverageStoreException(Errors.format(Errors.Keys.UnsupportedMultiOccurrence_1, GridCoverage.class)); } final boolean isNetcdfHack = imageWriter.getClass().getName().equals("org.geotoolkit.image.io.plugin.NetcdfImageWriter"); // TODO: DEPRECATED: to be removed in Apache SIS. final boolean isTiffHack = imageWriter.getClass().getName().equals("org.geotoolkit.image.io.plugin.TiffImageWriter"); /* * Convert the geodetic coordinates to pixel coordinates. */ final ImageWriteParam imageParam; try { imageParam = createImageWriteParam(image); } catch (IOException e) { throw new CoverageStoreException(formatErrorMessage(e), e); } MathTransform2D destToExtractedGrid = null; PlanarImage toDispose = null; if (param != null) { /* * Now convert the GridCoverageWriteParam values to ImageWriteParam value. * First of all, convert the ISO 119123 InterpolationMethod code to the JAI * code. */ final int interp; final InterpolationMethod interpolation = param.getInterpolation(); if (interpolation.equals(InterpolationMethod.NEAREST_NEIGHBOUR)) { interp = Interpolation.INTERP_NEAREST; } else if (interpolation.equals(InterpolationMethod.BILINEAR)) { interp = Interpolation.INTERP_BILINEAR; } else if (interpolation.equals(InterpolationMethod.BICUBIC)) { interp = Interpolation.INTERP_BICUBIC; } else { throw new CoverageStoreException(Errors.getResources(locale).getString( Errors.Keys.IllegalArgument_2, "interpolation", interpolation.name())); } destToExtractedGrid = geodeticToPixelCoordinates(gridGeometry, param, imageParam, isNetcdfHack); imageParam.setSourceBands(param.getSourceBands()); final Rectangle sourceRegion = imageParam.getSourceRegion(); final Rectangle requestRegion = requestedBounds; if (interp != Interpolation.INTERP_NEAREST || !isIdentity(destToExtractedGrid) || isGreater(requestRegion.width, imageParam.getSourceXSubsampling(), sourceRegion.width) || isGreater(requestRegion.height, imageParam.getSourceYSubsampling(), sourceRegion.height)) { /* * We need to resample the image if: * * - The transform from the source grid to the target grid is not affine; * - The above transform is affine but more complex than scale and translations; * - The translation or scale factors of the above transform are not integers; * - The requested envelope is greater than the coverage envelope; */ final InternationalString name = (coverage instanceof AbstractCoverage) ? ((AbstractCoverage) coverage).getName() : null; final ImageLayout layout = new ImageLayout( requestRegion.x, requestRegion.y, requestRegion.width, requestRegion.height); /* * Some codecs (e.g. JPEG) require that the whole image is available * as a single raster. */ layout.setTileWidth (requestRegion.width); layout.setTileHeight(requestRegion.height); final RenderingHints hints = new RenderingHints(JAI.KEY_IMAGE_LAYOUT, layout); destToExtractedGrid = (MathTransform2D) destGridToSource; // Will be used for logging purpose. final Warp warp; try { warp = WarpFactory.DEFAULT.create(name, destToExtractedGrid, sourceRegion); } catch (TransformException e) { throw new CoverageStoreException(formatErrorMessage(e), e); } if (DEBUG) { /* * To be enabled only when debugging. * Simplified output example from the writeSubsampledRegion() test: * * Grid to source: ┌ ┐ * │ 2 0 9 │ * │ 0 3 9 │ * │ 0 0 1 │ * └ ┘ * Source region: Rectangle[x=9, y=9, width=9, height=15] * Warp origin: [9.5, 10.0] * * If we had no scale factor, the Warp origin would be the same than the * translations. If we have scale factors be were mapping pixel corners, * then the warp origin would also be the same. * * But the JAI Warp operation maps pixel center. It does so by adding 0.5 * to pixel coordinates before applying the Warp, and removing 0.5 to the * result (see WarpTransform2D.getWarp(...) javadoc). This is actually the * desired behavior, as we can see with the picture below which represents * only the first pixel of the destination image. The cell are the source * pixels, the transform is the above matrix, and the coordinates are * relative to the source grid: * * (9,9) * ┌─────┬─────┐ * │ │ │ * ├─────┼─────┤ * │ (10,10.5) │ after the -0.5 final offset, become (9.5, 10). * ├─────┼─────┤ * │ │ │ * └─────┴─────┘ * (11,12) */ Object tr = destToExtractedGrid; if (tr instanceof LinearTransform) { tr = ((LinearTransform) tr).getMatrix(); } final TableWriter table = new TableWriter(null, 1); table.setMultiLinesCells(true); table.writeHorizontalSeparator(); table.write("Warping coverage:"); table.nextColumn(); table.write(String.valueOf(name)); table.nextLine(); table.write("Grid to source:"); table.nextColumn(); table.write(String.valueOf(tr)); table.nextLine(); table.write("Source region:"); table.nextColumn(); table.write(String.valueOf(sourceRegion)); table.nextLine(); table.write("Warp origin:"); table.nextColumn(); table.write(Arrays.toString(warp.warpPoint(0, 0, null))); table.nextLine(); table.writeHorizontalSeparator(); System.out.println(table); } double[] backgroundValues = param.getBackgroundValues(); if (backgroundValues == null) { backgroundValues = CoverageUtilities.getBackgroundValues(coverage); } image = toDispose = WarpDescriptor.create(image, warp, Interpolation.getInstance(interp), backgroundValues, hints); imageParam.setSourceRegion(null); imageParam.setSourceSubsampling(1, 1, 0, 0); } /* * Set other parameters inferred from the GridCoverageWriteParam. */ if (imageParam.canWriteCompressed()) { final Float compression = param.getCompressionQuality(); if (compression != null) { imageParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); imageParam.setCompressionQuality(compression); } } } if (imageParam.canWriteTiles()) { imageParam.setTilingMode(ImageWriteParam.MODE_EXPLICIT); //-- one destination tile equals source image tile representation imageParam.setTiling(image.getTileWidth() / imageParam.getSourceXSubsampling(), image.getTileHeight() / imageParam.getSourceYSubsampling(), 0, 0); } /* * Creates metadata with the information calculated so far. The code above this * point should have created an image having a grid geometry matching the user * request, so we will write that user request in the metadata. */ final ImageTypeSpecifier imageType = ImageTypeSpecifier.createFromRenderedImage(image); final IIOMetadata streamMetadata = isFirst ? imageWriter.getDefaultStreamMetadata(imageParam) : null; final IIOMetadata imageMetadata = imageWriter.getDefaultImageMetadata(imageType, imageParam); if (imageMetadata != null && ArraysExt.contains(imageMetadata.getMetadataFormatNames(), GEOTK_FORMAT_NAME)) { CoordinateReferenceSystem crs = null; Envelope env = null; double[] res = null; if (param != null) { crs = param.getCoordinateReferenceSystem(); env = param.getEnvelope(); res = param.getResolution(); } if (crs == null && gridGeometry.isDefined(GridGeometry2D.CRS)) { if (imageWriter instanceof MultidimensionalImageStore || isNetcdfHack || isTiffHack) { crs = gridGeometry.getCoordinateReferenceSystem(); } else { crs = gridGeometry.getCoordinateReferenceSystem2D(); } } if (env == null && gridGeometry.isDefined(GridGeometry2D.ENVELOPE)) { if (imageWriter instanceof MultidimensionalImageStore || isNetcdfHack || isTiffHack) { env = gridGeometry.getEnvelope(); } else { env = gridGeometry.getEnvelope2D(); } } if (crs != null) { final ReferencingBuilder builder = new ReferencingBuilder(imageMetadata); builder.setCoordinateReferenceSystem(crs); } if (env != null) { final GridDomainAccessor accessor = new GridDomainAccessor(imageMetadata); final Dimension size = getImageSize(image, imageParam); final double ymax = env.getMaximum(Y_DIMENSION); final double[] origin = env.getLowerCorner().getCoordinate(); final int dim = origin.length; origin[Y_DIMENSION] = ymax; if (res != null) { accessor.setOrigin(origin); final double[] p = new double[dim]; final double[] median = new double[dim]; for (int i = 0; i < dim; i++) { Arrays.fill(p, 0); if (i == X_DIMENSION) { p[i] = +res[X_DIMENSION]; } else if (i == Y_DIMENSION) { p[i] = -res[Y_DIMENSION]; } else { p[i] = 1; } accessor.addOffsetVector(p); median[i] = env.getMedian(i); } accessor.setSpatialRepresentation(median, null, PixelOrientation.UPPER_LEFT); final int[] maxGrid = new int[dim]; Arrays.fill(maxGrid, 1); maxGrid[X_DIMENSION] = size.width - 1; maxGrid[Y_DIMENSION] = size.height - 1; accessor.setLimits(new int[dim], maxGrid); } else { final double[] envBounds = env.getUpperCorner().getCoordinate(); envBounds[Y_DIMENSION] = env.getMinimum(Y_DIMENSION); final int[] high = new int[dim]; Arrays.fill(high, 1); high[X_DIMENSION] = size.width - 1; high[Y_DIMENSION] = size.height - 1; accessor.setRectifiedGridDomain(origin, envBounds, null, high, null, false); accessor.setSpatialRepresentation(origin, envBounds, null, PixelOrientation.UPPER_LEFT); } } final int n = coverage.getNumSampleDimensions(); final DimensionAccessor accessor = new DimensionAccessor(imageMetadata); for (int i=0; i<n; i++) { final SampleDimension band = coverage.getSampleDimension(i); accessor.selectChild(accessor.appendChild()); if (band != null) { accessor.setDimension(band, locale); } } } /* * Now process to the coverage writing. If the coverage is the only image (i.e. is both * the first and the last image), then we will write everything in a single operation by * a call to ImageWriter.write(...). Otherwise we will need to use the * prepareWriteSequence() - writeSequence(...) - endWriteSequence() cycle. */ checkAbortState(); try { if (streamMetadata != null) { completeImageMetadata(streamMetadata, null); } completeImageMetadata(imageMetadata, coverage); final IIOImage iio = new IIOImage(image, null, imageMetadata); if (isFirst & isLast) { imageWriter.write(streamMetadata, iio, imageParam); } else { if (isFirst) { imageWriter.prepareWriteSequence(streamMetadata); } imageWriter.writeToSequence(iio, imageParam); if (isLast) { imageWriter.endWriteSequence(); } } } catch (IOException e) { throw new CoverageStoreException(formatErrorMessage(e), e); } checkAbortState(); /* * Finally, logs the operation after the last image if logging are enabled. * The log level will depend on how long it took to write every images in * the sequence. */ if (isLast && startTime != Long.MIN_VALUE) { final long time = System.nanoTime() - startTime; final Level level = getLogLevel(time); if (LOGGER.isLoggable(level)) { final Dimension size = getImageSize(image, imageParam); CoordinateReferenceSystem crs = null; if (param != null) { crs = param.getCoordinateReferenceSystem(); } ImageCoverageStore.logOperation(level, locale, ImageCoverageWriter.class, true, output, 0, coverage, size, crs, destToExtractedGrid, time); } } if (toDispose != null) { toDispose.dispose(); } } /** * Returns the size of the image to be written. * * @param image The image to be written. * @param param The parameter to use for controlling the writing process, or {@code null}. * @return The size of the image being written. */ private static Dimension getImageSize(final RenderedImage image, final ImageWriteParam param) { final Dimension size = new Dimension(image.getWidth(), image.getHeight()); if (param != null) { final Rectangle request = param.getSourceRegion(); if (request != null) { size.width = Math.min(size.width, request.width / param.getSourceXSubsampling()); size.height = Math.min(size.height, request.height / param.getSourceXSubsampling()); } } return size; } /** * Returns {@code true} if the given request dimension scaled by the given subsampling * is greater than the given source dimension. * * @param request The span of the requested destination region. * @param subsampling The subsampling to be applied when reading the source. * @param source The span of the source region. */ private static boolean isGreater(final int request, final int subsampling, final int source) { return request * subsampling - (subsampling - 1) > source; } /** * Cancels the write operation. The default implementation forward the call to the * {@linkplain #imageWriter image writer}, if any. The content of the coverage * following the abort will be undefined. */ @Override public void abort() { super.abort(); final ImageWriter imageWriter = this.imageWriter; // Protect from changes. if (imageWriter != null) { imageWriter.abort(); } } /** * Returns an error message for the given exception. If the {@linkplain #output output} is * known, this method returns "<cite>Can't write {the name}</cite>" followed by the cause * message. Otherwise it returns the localized message of the given exception. */ @Override final String formatErrorMessage(final Throwable e) { return formatErrorMessage(output, e, true); } /** * Closes the output used by the {@link ImageWriter}, provided that the stream was not * given explicitly by the user. The {@link ImageWriter} is not disposed, so it can be * reused for the next image to write. * * @throws IOException if an error occurs while closing the output. */ private void close() throws IOException { final Object oldOutput = output; output = null; // Clear now in case the code below fails. final ImageWriter imageWriter = this.imageWriter; // Protect from changes. if (imageWriter != null) { if (imageWriter.getOutput() != oldOutput) { XImageIO.close(imageWriter); } else { imageWriter.setOutput(null); } } } /** * {@inheritDoc} * * @see ImageWriter#reset() */ @Override public void reset() throws CoverageStoreException { try { close(); } catch (IOException e) { throw new CoverageStoreException(formatErrorMessage(e), e); } if (imageWriter != null) { imageWriter.reset(); } super.reset(); } /** * Allows any resources held by this writer to be released. The result of calling any other * method subsequent to a call to this method is undefined. * <p> * The default implementation closes the {@linkplain #imageWriter image writer} output if * the later is a stream, then {@linkplain ImageWriter#dispose() disposes} that writer. * * @see ImageWriter#dispose() */ @Override public void dispose() throws CoverageStoreException { try { close(); } catch (IOException e) { throw new CoverageStoreException(formatErrorMessage(e), e); } if (imageWriter != null) { imageWriter.dispose(); imageWriter = null; } super.dispose(); } }