/* * Geotoolkit.org - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2012, Open Source Geospatial Foundation (OSGeo) * (C) 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.image.io.plugin; import java.util.List; import java.util.Arrays; import java.util.ArrayList; import java.util.Iterator; import java.util.Locale; import java.io.IOException; import java.awt.image.Raster; import java.awt.image.RenderedImage; import javax.imageio.IIOImage; import javax.imageio.ImageWriter; import javax.imageio.ImageWriteParam; import javax.imageio.metadata.IIOMetadata; import javax.imageio.metadata.IIOMetadataNode; import org.w3c.dom.Node; import ucar.nc2.NetcdfFileWriteable; import org.opengis.metadata.Metadata; import org.apache.sis.util.ArraysExt; import org.apache.sis.util.collection.BackingStoreException; import org.geotoolkit.image.io.FileImageWriter; import org.geotoolkit.metadata.netcdf.NetcdfMetadataWriter; import static org.geotoolkit.image.io.plugin.NetcdfImageReader.Spi.*; import static org.geotoolkit.image.io.metadata.SpatialMetadataFormat.ISO_FORMAT_NAME; import static org.geotoolkit.image.io.metadata.SpatialMetadataFormat.GEOTK_FORMAT_NAME; /** * Base implementation for NetCDF writers. * * {@section Support of related formats} * This implementation uses the <a href="http://www.unidata.ucar.edu/software/netcdf-java/">UCAR * NetCDF library</a> for writing data. Consequently, it can be used for writing other formats * supported by that library. * * @author Johann Sorel (Geomatys) * @author Martin Desruisseaux (Geomatys) * @version 3.20 * * @since 3.20 * @module */ public class NetcdfImageWriter extends FileImageWriter { /** * The NetCDF file where to write the images. This field is set to a non-null value * by {@link #prepareWriteSequence(IIOMetadata)}, then reset to a null value by * {@link #endWriteSequence()}. Whatever this field is null or not determines * whatever a sequence of write operations started or not. */ private NetcdfFileWriteable ncFile; /** * The list of NetCDF dimensions inferred from the CRS definition. */ private final List<NetcdfDimension> dimensions; /** * The list of image to write. Each image can be written in one or many NetCDF variables. */ private final List<NetcdfImage> images; /** * The object to use for writing metadata, created only if needed. */ private transient NetcdfMetadataWriter metadataWriter; /** * Constructs a new NetCDF writer. * * @param spi The service provider. */ public NetcdfImageWriter(final Spi spi) { super(spi != null ? spi : new Spi()); dimensions = new ArrayList<>(4); images = new ArrayList<>(4); } /** * Ensures that the NetCDF file is either open (if {@code open} is {@code true}), * or not already opened (if {@code open} is {@code false}). * * @param open Whatever the NetCDF file needs to be open or note. * @throws IllegalStateException If the NetCDF file is not in the expected state. */ private void ensureState(final boolean open) throws IllegalStateException { if ((ncFile != null) != open) { throw new IllegalStateException(open ? "prepareWriteSequence(...) must be invoked first." : "A sequence is already in progress."); // TODO: localize } } /** * Returns {@code true} since this writer can append many images in the same stream. * * @return Always {@code true} in this default implementation. */ @Override public boolean canWriteSequence() { return true; } /** * Writes a complete NetCDF file containing a single image and associated metadata. * The default implementation delegates to the following methods, in that order: * <p> * <ul> * <li>{@link #prepareWriteSequence(IIOMetadata)}</li> * <li>{@link #writeToSequence(IIOImage, ImageWriteParam)}</li> * <li>{@link #endWriteSequence()}</li> * </ul> * <p> * The output must have been set beforehand using the {@link #setOutput(Object)} method. The * given {@code IIOImage} may contain either a {@link RenderedImage} or a {@link Raster} source. * * @param metadata The stream metadata to be used for writing global attributes, or {@code null}. * @param image The image to write. While not mandatory, metadata are recommended in order to * specify the geographic location. * @param param The parameter controlling the writing process, or {@code null}. * @throws IllegalStateException If the {@linkplain #output output} has not been set, or if a * sequence is already in process of being written. * @throws IOException If an error occurs during writing. */ @Override public void write(final IIOMetadata metadata, final IIOImage image, final ImageWriteParam param) throws IOException { prepareWriteSequence(metadata); writeToSequence(image, param); endWriteSequence(); } /** * Prepares the NetCDF file to accept a series of subsequent {@link #writeToSequence(IIOImage, * ImageWriteParam)} calls. Any necessary header information is included using the information * provided in the metadata, if non-null. * <p> * The output must have been set beforehand using the {@link #setOutput(Object)} method. * * @param metadata The stream metadata to be used for writing global attributes, or {@code null}. * @throws IllegalStateException If the {@linkplain #output output} has not been set, or if a * sequence is already in process of being written. * @throws IOException If an error occurs during writing. */ @Override public void prepareWriteSequence(final IIOMetadata metadata) throws IOException { ensureState(false); if (output instanceof NetcdfFileWriteable) { ncFile = (NetcdfFileWriteable) output; } else { ncFile = NetcdfFileWriteable.createNew(getOutputPath().toString(), false); } writeMetadata(metadata); } /** * Writes the given metadata, if non-null. The NetCDF file must be in "define" mode. * This method may be invoked more than once, in which case the metadata will be merged * with precedence given to the first occurrences. */ private void writeMetadata(final IIOMetadata metadata) throws IOException { if (metadata != null) try { if (ArraysExt.contains(metadata.getExtraMetadataFormatNames(), ISO_FORMAT_NAME)) { final Node root = metadata.getAsTree(ISO_FORMAT_NAME); if (root instanceof IIOMetadataNode) { final Object userObject = ((IIOMetadataNode) root).getUserObject(); if (userObject instanceof Metadata) { if (metadataWriter == null) { metadataWriter = new NetcdfMetadataWriter(ncFile, this); } metadataWriter.write((Metadata) userObject); } } } } catch (BackingStoreException e) { throw e.unwrapOrRethrow(IOException.class); } } /** * Appends a single image and possibly associated metadata to the NetCDF file. * The {@link #prepareWriteSequence(IIOMetadata)} method must have been called * beforehand, or an {@link IllegalStateException} is thrown. * <p> * The given {@code IIOImage} may contain either a {@link RenderedImage} or a {@link Raster} * source. An {@link ImageWriteParam} may optionally be supplied to control the writing process. * * @param image The image to write. While not mandatory, metadata are recommended in order to * specify the geographic location. * @param param The parameter controlling the writing process, or {@code null}. * @throws IllegalStateException If the {@link #prepareWriteSequence(IIOMetadata)} * has not been invoked. * @throws IOException If an error occurs during writing. */ @Override public void writeToSequence(final IIOImage image, final ImageWriteParam param) throws IOException { ensureState(true); writeMetadata(image.getMetadata()); int i = dimensions.size(); final NetcdfImage data = new NetcdfImage(this, image, param, dimensions, createRectIter(image, param)); final int upper = dimensions.size(); while (i < upper) { // If any new dimension were added as a side effect of the NetcdfImage construction, // add them to the NetCDF file now. The intend is to get an error sooner if something // goes wrong with the NetCDF dimension creation. dimensions.get(i++).create(ncFile, locale); } data.createVariables(ncFile); images.add(data); } /** * Completes the writing of a sequence of images begun with {@link #prepareWriteSequence(IIOMetadata)}. * In current implementation, most of the actual NetCDF writing process happen here, so this method may * be long to execute. * * @throws IOException If an error occurs during writing. */ @Override public void endWriteSequence() throws IOException { ensureState(true); /* * Physically write the NetCDF dimensions, variables and attributes. After this method * call, we are no longer in "define" mode, so only the actual value of NetCDF variables * can be written. */ ncFile.create(); for (final Iterator<NetcdfDimension> it=dimensions.iterator(); it.hasNext();) { it.next().write(ncFile); it.remove(); // Allow the garbage collector to do its work. } for (final Iterator<NetcdfImage> it=images.iterator(); it.hasNext();) { it.next().write(ncFile); it.remove(); // Allow the garbage collector to do its work. } close(); } /** * Closes the underlying NetCDF file, unless it was provided by the user himself. * * @throws IOException If an error occurred while closing the file. */ @Override protected void close() throws IOException { metadataWriter = null; try { if (ncFile != null && ncFile != output) { ncFile.close(); } } finally { ncFile = null; images.clear(); dimensions.clear(); super.close(); } } /** * The service provider for {@code NetcdfImageWriter}. This SPI provides * necessary implementation for creating default {@link NetcdfImageWriter}. * <p> * The default constructor initializes the fields to the values listed below. * Users wanting different values should create a subclass of {@code Spi} and * set the desired values in their constructor. * <p> * <table border="1" cellspacing="0"> * <tr bgcolor="lightblue"><th>Field</th><th>Value</th></tr> * <tr><td> {@link #names}  </td><td> {@code "NetCDF"} </td></tr> * <tr><td> {@link #MIMETypes}  </td><td> {@code "application/netcdf"}, {@code "application/x-netcdf"} </td></tr> * <tr><td> {@link #pluginClassName}  </td><td> {@code "org.geotoolkit.image.io.plugin.NetcdfImageWriter"} </td></tr> * <tr><td> {@link #vendorName}  </td><td> {@code "Geotoolkit.org"} </td></tr> * <tr><td> {@link #version}  </td><td> Value of {@link org.geotoolkit.util.Version#GEOTOOLKIT} </td></tr> * <tr><td colspan="2" align="center">See super-class javadoc for remaining fields</td></tr> * </table> * * @author Johann Sorel (Geomatys) * @author Martin Desruisseaux (Geomatys) * @version 3.20 * * @since 3.20 * @module */ public static final class Spi extends FileImageWriter.Spi { /** * Constructs a default {@code NetcdfImageWriter.Spi}. The fields are initialized as * documented in the <a href="#skip-navbar_top">class javadoc</a>. Subclasses can * modify those values if desired. * <p> * For efficiency reasons, the fields are initialized to shared arrays. * Subclasses can assign new arrays, but should not modify the default array content. */ public Spi() { names = NAMES; MIMETypes = MIME_TYPES; suffixes = SUFFIXES; pluginClassName = "org.geotoolkit.image.io.plugin.NetcdfImageWriter"; readerSpiNames = new String[] {"org.geotoolkit.image.io.plugin.NetcdfImageReader$Spi"}; final int length = outputTypes.length; outputTypes = Arrays.copyOf(outputTypes, length+1); outputTypes[length] = NetcdfFileWriteable.class; nativeStreamMetadataFormatName = NATIVE_FORMAT_NAME; nativeImageMetadataFormatName = NATIVE_FORMAT_NAME; addExtraMetadataFormat(GEOTK_FORMAT_NAME, true, true); addExtraMetadataFormat(ISO_FORMAT_NAME, true, false); } /** * Returns a description for this provider. * * @todo Localize */ @Override public String getDescription(Locale locale) { return "NetCDF image encoder"; } /** * Returns an instance of the {@code NetcdfImageWriter} implementation associated * with this service provider. * * @param extension An optional extension object, which may be null. * @return An image writer instance. * @throws IOException if the attempt to instantiate the writer fails. */ @Override public ImageWriter createWriterInstance(final Object extension) throws IOException { return new NetcdfImageWriter(this); } } }