/*
* 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.Arrays;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.CharArrayWriter;
import java.awt.image.DataBuffer;
import java.awt.image.WritableRaster;
import javax.media.jai.RasterFactory;
import org.opengis.geometry.Envelope;
import org.opengis.referencing.datum.PixelInCell;
import org.opengis.referencing.cs.CoordinateSystem;
import org.opengis.referencing.cs.CoordinateSystemAxis;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.geotoolkit.test.TestData;
import org.geotoolkit.test.image.ImageTestBase;
import org.geotoolkit.coverage.grid.GridCoverage2D;
import org.geotoolkit.coverage.grid.GridGeometry2D;
import org.geotoolkit.coverage.grid.GridCoverageBuilder;
import org.geotoolkit.coverage.io.CoverageIO;
import org.geotoolkit.coverage.io.GridCoverageReader;
import org.geotoolkit.coverage.io.ImageCoverageWriter;
import org.geotoolkit.coverage.io.CoverageStoreException;
import org.geotoolkit.factory.Hints;
import org.apache.sis.geometry.GeneralEnvelope;
import org.apache.sis.internal.referencing.GeodeticObjectBuilder;
import org.apache.sis.referencing.CRS;
import org.geotoolkit.referencing.operation.matrix.GeneralMatrix;
import org.apache.sis.referencing.CommonCRS;
import org.apache.sis.referencing.crs.AbstractCRS;
import org.apache.sis.referencing.cs.AxesConvention;
import ucar.nc2.NCdumpW;
import org.junit.*;
import static org.geotoolkit.test.Assert.*;
/**
* Tests writing a NetCDF file through the {@link ImageCoverageWriter} API.
*
* @author Johann Sorel (Geomatys)
* @author Martin Desruisseaux (Geomatys)
* @version 3.20
*
* @since 3.20
*/
public class NetcdfCoverageWriterTest extends ImageTestBase {
/**
* Necessary for some tests for now because GeographicBoundingBox.setBounds(Envelope)
* does not have the possibility to specify whether it wants a lenient or non-lenient
* factory.
*/
static {
Hints.putSystemDefault(Hints.LENIENT_DATUM_SHIFT, Boolean.TRUE);
}
/**
* Tolerance threshold for floating point number comparisons.
*/
private static final double EPS = 1E-7;
/**
* The images size, in pixels.
*/
private static final int WIDTH=4, HEIGHT=2;
/**
* Temporary variable for skipping read operations for some unsupported projections.
*/
private boolean skipRead;
/**
* Creates a new test case.
*/
public NetcdfCoverageWriterTest() {
super(NetcdfImageWriter.class);
}
/**
* Tests the creation of a NetCDF file using {@code "CRS:84"} on the whole world.
*
* @throws Exception If an I/O, CRS factory or coverage store error occurred.
*/
@Test
@Ignore("CDL has changed while upgrading NetCDF dependency to 4.3.21")
public void testCRS84() throws Exception {
final GeneralEnvelope env = new GeneralEnvelope(CommonCRS.WGS84.normalizedGeographic());
env.setRange(0, -180, 180);
env.setRange(1, -90, 90);
testWriteRead(env, "CRS84.cdl");
}
/**
* Tests the creation of a NetCDF file using {@code "EPSG:4326"} on the whole world.
* This test writes the latitude axis before the longitude one.
*
* @throws Exception If an I/O, CRS factory or coverage store error occurred.
*/
@Test
@Ignore("CDL has changed while upgrading NetCDF dependency to 4.3.21")
public void testEPSG4326() throws Exception {
final GeneralEnvelope env = new GeneralEnvelope(CommonCRS.WGS84.geographic());
env.setRange(0, -90, 90);
env.setRange(1, -180, 180);
testWriteRead(env, "EPSG4326.cdl");
}
/**
* Tests the creation of a NetCDF file using {@code "EPSG:4088"}.
* This is a Equidistant projection, which is more suitable for testing than
* more complex projections because of the potential for rounding errors.
*
* @throws Exception If an I/O, CRS factory or coverage store error occurred.
*/
@Test
@Ignore("CDL has changed while upgrading NetCDF dependency to 4.3.21")
public void testEPSG4088() throws Exception {
final GeneralEnvelope env = new GeneralEnvelope(CRS.forCode("EPSG:4088"));
env.setRange(0, -2E7, 2E7);
env.setRange(1, -1E7, 1E7);
skipRead = true;
testWriteRead(env, "EPSG4088.cdl");
}
/**
* Tests the creation of a NetCDF file with the (indirectly) given CRS definition.
* Then reads the NetCDF file back and ensures that Geotk properly found the CRS back,
* or at least something close.
*
* @param envelope The geographic envelope. The envelope CRS is the CRS to be encoded in the NetCDF.
* @param cdlFile The name (without path) of the file containing the expected NetCDF content
* expressed in Common Data Language (CDL).
*/
private void testWriteRead(final Envelope envelope, final String cdlFile) throws IOException, CoverageStoreException {
final GridCoverage2D coverage = writeAndRead(cdlFile, createGridCoverage(envelope, "data", 0));
verifyGridGeometry(coverage, envelope);
assertSampleValuesEqual(cdlFile, image, coverage.getRenderedImage(), EPS);
}
/**
* Tests writing a sequence of 3 variables.
*
* @throws Exception If an I/O, CRS factory or coverage store error occurred.
*/
@Test
@Ignore("CDL has changed while upgrading NetCDF dependency to 4.3.21")
public void testSequence() throws Exception {
final GeneralEnvelope env = new GeneralEnvelope(CommonCRS.WGS84.normalizedGeographic());
env.setRange(0, -180, 180);
env.setRange(1, -90, 90);
final GridCoverage2D coverage = writeAndRead("sequence.cdl",
createGridCoverage(env, "cat1", 100),
createGridCoverage(env, "cat2", 200),
createGridCoverage(env, "cat3", 300));
verifyGridGeometry(coverage, env);
assertSampleValuesEqual("Sequence", image, coverage.getRenderedImage(), EPS);
}
/**
* Tests the creation of a three-dimensional NetCDF file. This is similar to
* {@link #testSequence()}, except that the coverages have the same name.
*
* @throws Exception If an I/O, CRS factory or coverage store error occurred.
*/
@Test
@Ignore("Not yet implemented")
public void testXYZ() throws Exception {
final GeneralEnvelope env = new GeneralEnvelope(AbstractCRS.castOrCopy(CommonCRS.WGS84.geographic3D()).forConvention(AxesConvention.RIGHT_HANDED));
env.setRange(0, -180, 180);
env.setRange(1, -90, 90);
env.setRange(2, 10, 12); final GridCoverage2D coverage1 = createGridCoverage(env, "data", 100);
env.setRange(2, 20, 22); final GridCoverage2D coverage2 = createGridCoverage(env, "data", 200);
env.setRange(2, 30, 32); final GridCoverage2D coverage3 = createGridCoverage(env, "data", 300);
final GridCoverage2D coverage = writeAndRead("xyz.cdl", coverage1, coverage2, coverage3);
verifyGridGeometry(coverage, env);
}
/**
* Tests the creation of a four-dimensional NetCDF file.
*
* @throws Exception If an I/O, CRS factory or coverage store error occurred.
*/
@Test
@Ignore("Not yet implemented")
public void testXYZT() throws Exception {
final CoordinateReferenceSystem crs = new GeodeticObjectBuilder().addName("WGS84 + z + t")
.createCompoundCRS(AbstractCRS.castOrCopy(CommonCRS.WGS84.geographic3D()).forConvention(AxesConvention.RIGHT_HANDED),
CommonCRS.Temporal.JAVA.crs());
final GeneralEnvelope env = new GeneralEnvelope(crs);
env.setRange(0, -180, 180);
env.setRange(1, -90, 90);
env.setRange(2, 10, 10);
env.setRange(3, 3, 3);
final GridCoverage2D coverage1 = createGridCoverage(env, "data", 100);
env.setRange(2, 20, 20);
env.setRange(3, 6, 6);
final GridCoverage2D coverage2 = createGridCoverage(env, "data", 200);
env.setRange(2, 30, 30);
env.setRange(3, 9, 9);
final GridCoverage2D coverage3 = createGridCoverage(env, "data", 300);
final GridCoverage2D coverage = writeAndRead("xyzt.cdl", coverage1, coverage2, coverage3);
verifyGridGeometry(coverage, env);
}
/**
* Writes the given coverage as a NetCDF file, ensures that the CDL is equals to the given one,
* then reads the coverage back.
*
* @param cdlFile The name (without path) of the file containing the expected NetCDF content
* expressed in Common Data Language (CDL).
* @param coverages The coverages to write.
* @return The coverage which has been read back.
*/
private GridCoverage2D writeAndRead(final String cdlFile, final GridCoverage2D... coverages)
throws IOException, CoverageStoreException
{
final GridCoverage2D coverage;
final File tempFile = File.createTempFile("test", ".nc");
try {
CoverageIO.write(Arrays.asList(coverages), "NetCDF", tempFile);
assertEqualsCDL(cdlFile, tempFile);
if (skipRead) return coverages[0]; // Temporary workaround for unsupported projections.
final GridCoverageReader reader = CoverageIO.createSimpleReader(tempFile);
coverage = (GridCoverage2D) reader.read(0, null);
reader.dispose();
} finally {
tempFile.delete();
}
return coverage;
}
/**
* Creates a new grid coverage using the given envelope and a single band of the given name.
* The coverages created by this method are used by the {@link #testWriteRead(Envelope, String)}
* method in order to write a NetCDF file. The coverage needs to be small, because the sample
* values will be formatted as Common Data Language (CDL) string.
* <p>
* The image created by this method is stored in the {@link #image} field.
*
* @param envelope The geographic envelope.
* @param variableName The name of the single band.
* @param firstValue The value in the upper-left corner.
* @return The grid coverage.
*/
private GridCoverage2D createGridCoverage(final Envelope envelope, final String variableName, final double firstValue) {
/*
* Define the sample values from 0 inclusive to WIDTH*HEIGHT exclusive, in the order they
* will be written in the NetCDF file. This has the consequence of putting the 0 value in
* the lower-left corner, since NetCDF file typically use the geometric axis directions.
*/
final WritableRaster raster = RasterFactory.createBandedRaster(DataBuffer.TYPE_FLOAT, WIDTH, HEIGHT, 1, null);
for (int inc=0,y=0; y<HEIGHT; y++) {
for (int x=0; x<WIDTH; x++) {
raster.setSample(x, y, 0, firstValue + inc++);
}
}
/*
* Define the 'gridToCRS' transform.
*/
final int dim = envelope.getDimension();
final GeneralMatrix matrix = new GeneralMatrix(dim+1, dim+1);
matrix.setElement(0, 0, envelope.getSpan(0) / WIDTH);
matrix.setElement(1, 1, envelope.getSpan(1) / HEIGHT);
for (int i=0; i<dim; i++) {
matrix.setElement(i, dim, envelope.getMinimum(i));
}
/*
* Build the coverage.
*/
final GridCoverageBuilder builder = new GridCoverageBuilder();
builder.variable(0).setName(variableName);
builder.variable(0).setColors("grayscale");
builder.setGridToCRS(matrix);
builder.setPixelAnchor(PixelInCell.CELL_CORNER);
builder.setCoordinateReferenceSystem(envelope.getCoordinateReferenceSystem());
builder.setRenderedImage(raster);
final GridCoverage2D coverage = builder.getGridCoverage2D();
if (image == null) { // Remember only the first image.
image = coverage.getRenderedImage();
}
return coverage;
}
/**
* Verifies the CRS and envelope of the given coverage against the given expected values.
*
* @param coverage The coverage to verify.
* @param envelope The expected envelope, which must contain the expected CRS.
*/
private void verifyGridGeometry(final GridCoverage2D coverage, final Envelope envelope) {
final GridGeometry2D gridGeom = coverage.getGridGeometry();
final CoordinateReferenceSystem actualCRS = gridGeom.getCoordinateReferenceSystem();
assertNotNull(actualCRS);
/*
* We can not perform the usual assertTrue(Utilitie.deepEquals(..., ComparisonMode.DEBUG)
* call, because the CRS read from the NetCDF file is not the usual DefaultGeographicCRS
* implementations. Instead we get the NetCDF wrappers, which are not directly comparable.
* We have to split the components.
*/
final CoordinateReferenceSystem expectedCRS = envelope.getCoordinateReferenceSystem();
final CoordinateSystem expectedCS = expectedCRS.getCoordinateSystem();
final CoordinateSystem candidateCS = actualCRS.getCoordinateSystem();
assertEquals(expectedCS.getDimension(), candidateCS.getDimension());
/*
* Compares each axis.
*/
for (int i=0,n=expectedCS.getDimension(); i<n; i++) {
final CoordinateSystemAxis candidateAxis = candidateCS.getAxis(i);
final CoordinateSystemAxis expectedAxis = expectedCS.getAxis(i);
assertEquals(expectedAxis.getDirection(), candidateAxis.getDirection());
assertEquals(expectedAxis.getUnit(), candidateAxis.getUnit());
}
/*
* Compares the envelopes.
*/
final Envelope candidateEnvope = coverage.getEnvelope();
assertEquals(envelope.getDimension(), candidateEnvope.getDimension());
final int dimension = envelope.getDimension();
for (int i=0; i<dimension; i++) {
assertEquals(envelope.getMinimum(i), candidateEnvope.getMinimum(i), EPS);
assertEquals(envelope.getMaximum(i), candidateEnvope.getMaximum(i), EPS);
}
}
/**
* Asserts that the header of the given NetCDF file has the structure defined by the given
* Common Data Language (CDL) string.
*
* @param cdlFile The filename (without path) of the expected content of the NetCDF file.
* @param netcdfFile The NetCDF file to inspect.
* @throws IOException If an I/O error occurred while reading the NetCDF file.
*/
private static void assertEqualsCDL(final String cdlFile, final File netcdfFile) throws IOException {
final CharArrayWriter buffer = new CharArrayWriter();
final PrintWriter writer = new PrintWriter(buffer);
NCdumpW.print(netcdfFile.getPath(), writer,
true, // If true, show all variables
false, // If true, show only coordinate variables
false, // If true, print NcML and ignore other arguments
true, // If true, print strict CDL representation
null, // Semi-colon delimited list of variables to print
null); // If non-null, allow task to be cancelled
final String expected = TestData.readText(NetcdfCoverageWriterTest.class, cdlFile);
String actual = buffer.toString();
actual = actual.substring(actual.indexOf('{')); // Trims the filename before '{'.
actual = actual.replace("\n\n", "\n");
assertMultilinesEquals(expected, actual);
}
}