package org.geotoolkit.image.io.plugin; import java.awt.Rectangle; import java.awt.image.BufferedImage; import java.awt.image.DataBuffer; import java.awt.image.DataBufferShort; import java.awt.image.WritableRaster; import java.io.File; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.util.Locale; import java.util.logging.Level; import javax.imageio.ImageReadParam; import javax.imageio.ImageReader; import javax.imageio.ImageTypeSpecifier; import javax.imageio.stream.ImageInputStream; import org.apache.sis.internal.referencing.j2d.AffineTransform2D; import org.apache.sis.internal.storage.ChannelImageInputStream; import org.apache.sis.measure.Units; import org.apache.sis.referencing.CommonCRS; import org.geotoolkit.coverage.grid.GeneralGridGeometry; import org.geotoolkit.coverage.grid.GridEnvelope2D; import org.geotoolkit.image.io.SpatialImageReader; import static org.geotoolkit.image.io.WarningProducer.LOGGER; import org.geotoolkit.image.io.metadata.ReferencingBuilder; import org.geotoolkit.image.io.metadata.SpatialMetadata; import org.geotoolkit.internal.image.io.DimensionAccessor; import org.geotoolkit.internal.image.io.GridDomainAccessor; import org.opengis.metadata.content.TransferFunctionType; import org.opengis.metadata.spatial.CellGeometry; import org.opengis.referencing.crs.GeographicCRS; import org.opengis.referencing.datum.PixelInCell; /** * GIMMS AVHRR Global NDVI 1/12-degree Geographic Lat/Lon. * * 1. DESCRIPTION * * VI3G dataset is an inverse cartographic transformation and mosaicing of the * GIMMS AVHRR 8-km Albers Conical Equal Area continentals AF, AZ, EA, NA, and * SA to a global 1/12-degree Lat/Lon grid. * * Continent demarcation and pixel selection is predetermined with an ancillary * NDVI-3G based land-water mask. * * 2. FILE NAMING CONVENTION * * geo[year][month][period].n[sat][-[VI][version]g * * where * year 2-int 2 digit year * month 3-char abbr. lower case month name * period 3-char alphanum period: bimonthly 15[ab] * sat 2-int satellite number * version n-int version number (3) * * For example, * * geo09jan15a.n17-VI3g * * 3. GRID PARAMETERS * * grid-name: Geographic Lat/Lon * pixel-size: 1/12=0.833 degrees * * size-x: 4320 * size-y: 2160 * * upper-left-lat: 90.0-1/24 * upper-left-lon: -180.0+1/24 * lower-right-lat: -90.0+1/24 * lower-right-lon: 180.0-1/24 * * *coordinates located UL corner of pixel * * 4. DATA FORMAT - VI3g * * datatype: 16-bit signed integer * byte-order: big endian * * scale-factor: 10000 * min-valid: -10000 * max-valid: 10000 * mask-water: -10000 * mask-nodata: -5000 * * *values include embedded flags (see full NDVI-3G documentation - in preparation) * * 5. FLAG VALUES * Each NDVI data set (ndvi3g) is an INT16 file saved with ieee-big_endian * it ranges from -10000->(10000->10004) * with the flagW file added to the ndvi values as follows: * ndvi3g = round(ndvi*10000) + flagW - 1; * flagW ranges from 1->7 * to retrieve the original ndvi and flagW values * flagW = ndvi3g-floor(ndvi3g/10)*10 + 1; * ndvi = floor(ndvi3g/10)/1000 * The meaning of the FLAG: * FLAG = 7 (missing data) * FLAG = 6 (NDVI retrieved from average seasonal profile, possibly snow) * FLAG = 5 (NDVI retrieved from average seasonal profile) * FLAG = 4 (NDVI retrieved from spline interpolation, possibly snow) * FLAG = 3 (NDVI retrieved from spline interpolation) * FLAG = 2 (Good value) * FLAG = 1 (Good value) * END * * @author Alexis Manin (Geomatys) */ public class VI3GReader extends SpatialImageReader { public static final int SAMPLE_SIZE = Short.SIZE / Byte.SIZE; public static final int WIDTH = 2160; public static final int HEIGHT = 4320; private static final ImageTypeSpecifier IMAGE_TYPE = ImageTypeSpecifier.createGrayscale(16, DataBuffer.TYPE_SHORT, true); private Path fileInput; /** * Constructs a new image reader. * * @param provider the {@link javax.imageio.spi.ImageReaderSpi} that is invoking this constructor, or null. */ public VI3GReader(Spi provider) { super(provider); } @Override public int getWidth(int imageIndex) throws IOException { checkImageIndex(imageIndex); if (fileInput == null) { throw new IOException("No valid input set."); } return WIDTH; } @Override public int getHeight(int imageIndex) throws IOException { checkImageIndex(imageIndex); if (fileInput == null) { throw new IOException("No valid input set."); } return HEIGHT; } /** * {@inheritDoc} */ @Override protected int getRawDataType(final int imageIndex) throws IOException { checkImageIndex(imageIndex); return IMAGE_TYPE.getSampleModel().getDataType(); } /** * {@inheritDoc} */ @Override public ImageTypeSpecifier getRawImageType(final int imageIndex) throws IOException { checkImageIndex(imageIndex); return IMAGE_TYPE; } @Override protected SpatialMetadata createMetadata(int imageIndex) throws IOException { if (imageIndex < 0) return null; SpatialMetadata md = new SpatialMetadata(false, this, null); final DimensionAccessor dac = new DimensionAccessor(md); dac.selectChild(dac.appendChild()); dac.setFillSampleValues(-10000, -5000); dac.setValidSampleValue(0, 10006); dac.setTransfertFunction(1, 0, TransferFunctionType.LINEAR); dac.setUnits(Units.UNITY.multiply(10000)); if (fileInput == null) { throw new IOException("No valid input set."); } try { // Set Geo-spatial information. GridDomainAccessor accessor = new GridDomainAccessor(md); final GeographicCRS geographicCRS = CommonCRS.WGS84.geographic(); final ReferencingBuilder builder = new ReferencingBuilder(md); builder.setCoordinateReferenceSystem(geographicCRS); // HACK : a problem has been detected with Geotk rendering of latitude first data. // As a warkaround, we define the image as longitude first, and then roll it // using sheer. final GridEnvelope2D gridEnv = new GridEnvelope2D(0, 0, getWidth(imageIndex), getHeight(imageIndex)); AffineTransform2D tr = new AffineTransform2D(-180.0 / WIDTH, 0, 0, 360.0 / HEIGHT, 90.0, -180.0); accessor.setGridGeometry( new GeneralGridGeometry(gridEnv, PixelInCell.CELL_CORNER, tr, geographicCRS), PixelInCell.CELL_CORNER, CellGeometry.AREA ); } catch (Exception e) { LOGGER.log(Level.WARNING, "Geo-spatial information cannot be retrieved from input " + input, e); } return md; } @Override public void setInput(Object input, boolean seekForwardOnly, boolean ignoreMetadata) { super.setInput(input, seekForwardOnly, ignoreMetadata); if (input instanceof Path) { fileInput = (Path) input; } else if (input instanceof File) { fileInput = ((File) input).toPath(); } else { fileInput = null; } } @Override public BufferedImage read(int imageIndex, ImageReadParam param) throws IOException { final Rectangle srcRegion = new Rectangle(); final Rectangle dstRegion = new Rectangle(); final BufferedImage image = getDestination(param, getImageTypes(imageIndex), WIDTH, HEIGHT); computeRegions(param, WIDTH, HEIGHT, image, srcRegion, dstRegion); readLayer(image.getRaster(), param, srcRegion, dstRegion); return image; } /** * Processes to the image reading, and stores the pixels in the given raster.<br/> * Process fill raster from informations stored in stripOffset made. * * @param raster The raster where to store the pixel values. * @param param Parameters used to control the reading process, or {@code null}. * @param srcRegion The region to read in source image. * @param dstRegion The region to write in the given raster. * @throws IOException If an error occurred while reading the image. */ private void readLayer(final WritableRaster raster, final ImageReadParam param, final Rectangle srcRegion, final Rectangle dstRegion) throws IOException { try (final ImageInputStream imageStream = getImageInputStream()) { final DataBufferShort dataBuffer = (DataBufferShort) raster.getDataBuffer(); final short[] data = dataBuffer.getData(0); final double stepX = (param == null)? 1 : param.getSourceXSubsampling(); final double stepY = (param == null)? 1 : param.getSourceYSubsampling(); // Current position (in byte) from source file. long srcBuffPos = (srcRegion.y * WIDTH + srcRegion.x) * SAMPLE_SIZE; long srcScanLineStride = WIDTH * SAMPLE_SIZE; // Current position in destination array (short) int destPosition = dstRegion.y * raster.getWidth() + dstRegion.x; int destScanLineStride = raster.getWidth(); final int srcMaxY = srcRegion.y + srcRegion.height; if (stepX == 1) { for (int y = srcRegion.y; y < srcMaxY; y += stepY) { imageStream.seek(srcBuffPos); imageStream.readFully(data, destPosition, srcRegion.width); // Prepare to go on the next line of source image srcBuffPos += srcScanLineStride * stepY; // Prepare to go on the next line of destination image destPosition += destScanLineStride; } } else { for (int y = srcRegion.y; y < srcMaxY; y += stepY) { int tmpDestPosition = destPosition; for (int i = 0; i < srcRegion.width; i += stepX, tmpDestPosition++) { imageStream.seek(srcBuffPos+(i*SAMPLE_SIZE)); imageStream.readFully(data, tmpDestPosition, 1); } // Prepare to go on the next line of source image srcBuffPos += srcScanLineStride * stepY; // Prepare to go on the next line of destination image destPosition += destScanLineStride; } } } } private ImageInputStream getImageInputStream() throws IOException { if (fileInput == null) throw new IOException("Input object is not a valid file or path."); final ChannelImageInputStream stream = new ChannelImageInputStream(null, Files.newByteChannel(fileInput, StandardOpenOption.READ), ByteBuffer.allocateDirect(8192), false); stream.setByteOrder(ByteOrder.BIG_ENDIAN); return stream; } public static class Spi extends SpatialImageReader.Spi { /** * The list of valid input types. */ private static final Class<?>[] INPUT_TYPES = new Class<?>[]{File.class, Path.class}; /** * Default list of file extensions. */ private static final String[] SUFFIXES = new String[]{"vi3g", "VI3G", "n07-VI3g"}; private static final String[] MIME_TYPES = new String[]{"application/x-ogc-vi3g"}; /** * Constructs a default {@code RawImageReader.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 = SUFFIXES; suffixes = SUFFIXES; MIMETypes = MIME_TYPES; inputTypes = INPUT_TYPES; pluginClassName = VI3GReader.class.getName(); // This reader does not support any metadata. nativeStreamMetadataFormatName = null; nativeImageMetadataFormatName = null; } @Override public String getDescription(Locale locale) { return "AVHRR VI3G format"; } @Override public boolean canDecodeInput(Object source) throws IOException { final Path temp; if (source instanceof File) { temp = ((File) source).toPath(); } else if (source instanceof Path) { temp = (Path) source; } else { return false; } return temp.getFileName().toString().toLowerCase().endsWith("vi3g"); } @Override public ImageReader createReaderInstance(final Object extension) throws IOException { return new VI3GReader(this); } } }