/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2007-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.coverageio.jp2k;
import it.geosolutions.imageio.plugins.jp2k.JP2KStreamMetadata;
import it.geosolutions.imageio.plugins.jp2k.box.UUIDBox;
import it.geosolutions.imageio.plugins.jp2k.box.UUIDBoxMetadataNode;
import java.awt.Rectangle;
import java.awt.geom.AffineTransform;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.nio.channels.FileChannel;
import java.util.Collection;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataNode;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream;
import org.geotools.coverage.CoverageFactoryFinder;
import org.geotools.coverage.grid.GridCoverage2D;
import org.geotools.coverage.grid.GridCoverageFactory;
import org.geotools.coverage.grid.GridEnvelope2D;
import org.geotools.coverage.grid.io.AbstractGridCoverage2DReader;
import org.geotools.coverage.grid.io.AbstractGridFormat;
import org.geotools.coverage.grid.io.imageio.geotiff.GeoTiffIIOMetadataDecoder;
import org.geotools.coverage.grid.io.imageio.geotiff.GeoTiffMetadata2CRSAdapter;
import org.geotools.data.DataSourceException;
import org.geotools.data.DataUtilities;
import org.geotools.data.PrjFileReader;
import org.geotools.data.WorldFileReader;
import org.geotools.factory.Hints;
import org.geotools.geometry.GeneralEnvelope;
import org.geotools.metadata.iso.spatial.PixelTranslation;
import org.geotools.referencing.CRS;
import org.geotools.referencing.operation.transform.ProjectiveTransform;
import org.geotools.resources.coverage.CoverageUtilities;
import org.opengis.coverage.grid.Format;
import org.opengis.coverage.grid.GridCoverage;
import org.opengis.coverage.grid.GridCoverageReader;
import org.opengis.coverage.grid.GridRange;
import org.opengis.geometry.Envelope;
import org.opengis.parameter.GeneralParameterValue;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.datum.PixelInCell;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.TransformException;
import com.sun.media.imageioimpl.plugins.tiff.TIFFImageReader;
import com.sun.media.imageioimpl.plugins.tiff.TIFFImageReaderSpi;
/**
* This class can read a JP2K data source and create a {@link GridCoverage2D}
* from the data.
*
* @author Daniele Romagnoli, GeoSolutions.
* @author Simone Giannecchini (simboss), GeoSolutions
*/
@SuppressWarnings("deprecation")
public final class JP2KReader extends AbstractGridCoverage2DReader implements
GridCoverageReader {
/** Logger. */
private final static Logger LOGGER = org.geotools.util.logging.Logging.getLogger(JP2KReader.class);
/** The system-dependent default name-separator character. */
private final static char SEPARATOR = File.separatorChar;
private final static short[] GEOJP2_UUID = new short[] { 0xb1, 0x4b, 0xf8, 0xbd, 0x08,
0x3d, 0x4b, 0x43, 0xa5, 0xae, 0x8c, 0xd7, 0xd5, 0xa6, 0xce, 0x03 };
private final static short[] MSIG_WORLDFILEBOX_UUID = new short[] { 0x96, 0xa9, 0xf1, 0xf1,
0xdc, 0x98, 0x40, 0x2d, 0xa7, 0xae, 0xd6, 0x8e, 0x34, 0x45, 0x18, 0x09 };
private final static int WORLD_FILE_INTERPRETATION_PIXEL_CORNER = 1;
/**
* The base {@link GridRange} for the {@link GridCoverage2D} of this reader.
*/
private GridEnvelope2D nativeGridRange = null;
private GeneralEnvelope nativeEnvelope = null;
ImageReaderSpi cachedSPI;
/**
* Creates a new instance of a {@link JP2KReader}. I assume nothing about
* file extension.
*
* @param input
* Source object for which we want to build an JP2KReader.
* @throws DataSourceException
*/
public JP2KReader(Object input) throws IOException {
this(input, null);
}
/**
* Setting Envelope, GridRange and CRS from the given {@code ImageReader}
*
* @param reader
* the {@code ImageReader} from which to retrieve metadata
* (if available) for setting properties
* @throws IOException
*/
protected void setCoverageProperties(ImageReader reader) throws IOException {
// //
//
// Getting stream metadata from the underlying layer
//
// //
final IIOMetadata metadata = reader.getStreamMetadata();
int hrWidth = reader.getWidth(0);
int hrHeight = reader.getHeight(0);
final Rectangle actualDim = new Rectangle(0, 0, hrWidth, hrHeight);
this.nativeGridRange = new GridEnvelope2D(actualDim);
if (this.crs == null) {
parsePRJFile();
}
if (this.nativeEnvelope == null)
parseWorldFile();
if (this.crs == null || this.nativeEnvelope == null)
checkUUIDBoxes(metadata);
// //
//
// If no sufficient information have been found to set the
// envelope, try other ways, such as looking for a WorldFile
//
// //
if (this.nativeEnvelope == null) {
throw new DataSourceException(
"Unavailable envelope for this coverage");
}
// setting the coordinate reference system for the envelope
originalEnvelope = getCoverageEnvelope();
originalEnvelope.setCoordinateReferenceSystem(crs);
originalGridRange = getCoverageGridRange();
// Additional settings due to "final" methods getOriginalXXX
}
/**
* @param nativeEnvelope
* the nativeEnvelope to set
*/
protected void setCoverageEnvelope(GeneralEnvelope coverageEnvelope) {
this.nativeEnvelope = coverageEnvelope;
}
/**
* @return the nativeEnvelope
*/
protected GeneralEnvelope getCoverageEnvelope() {
return nativeEnvelope;
}
/**
* @param nativeGridRange
* the nativeGridRange to set
*/
protected void setCoverageGridRange(GridEnvelope2D coverageGridRange) {
this.nativeGridRange = coverageGridRange;
}
/**
* @return the nativeGridRange
*/
protected GridEnvelope2D getCoverageGridRange() {
return nativeGridRange;
}
private boolean isGeoJP2(final byte[] id) {
return isSameUUID(id, GEOJP2_UUID);
}
private boolean isWorldBox(final byte[] id) {
return isSameUUID(id, MSIG_WORLDFILEBOX_UUID);
}
private boolean isSameUUID(final byte[] id, final short[] uuid) {
for (int i = 0; i < uuid.length; i++) {
if ((id[i] & 0xFF) != uuid[i])
return false;
}
return true;
}
/**
* Look for UUID boxes containing GeoJP2 Boxes / MSIG World Box
* @param metadata
* @throws IOException
*/
private void checkUUIDBoxes(final IIOMetadata metadata) throws IOException{
if (!(metadata instanceof JP2KStreamMetadata)) {
if (LOGGER.isLoggable(Level.FINE))
LOGGER.fine("Metadata should be an instance of the expected class:"
+ " JP2KStreamMetadata.");
return;
}
// //
//
// Looking for the UUIDBoxMetadataNode
//
// //
final List<IIOMetadataNode> uuidBoxMetadataNodes = ((JP2KStreamMetadata) metadata).searchOccurrencesNode(UUIDBox.BOX_TYPE);
UUIDBoxMetadataNode geoJP2uuid = null;
UUIDBoxMetadataNode worldBoxuuid = null;
if (uuidBoxMetadataNodes != null && !uuidBoxMetadataNodes.isEmpty()) {
for (IIOMetadataNode node : uuidBoxMetadataNodes) {
if (node instanceof UUIDBoxMetadataNode) {
final UUIDBoxMetadataNode uuid = (UUIDBoxMetadataNode) node;
final byte[] id = uuid.getUuid();
if (isGeoJP2(id)) {
geoJP2uuid = uuid;
continue;
}
if (isWorldBox(id)) {
worldBoxuuid = uuid;
}
}
}
}
if (geoJP2uuid!=null)
getGeoJP2(geoJP2uuid);
// //
//
// Without a proper crs, the World Box is useless
//
// //
if (worldBoxuuid!=null && crs != null){
getWorldBox(worldBoxuuid);
}
}
private void getWorldBox(final UUIDBoxMetadataNode uuid) throws IOException {
// //
//
// Parsing Header
//
// //
final byte[] bb = uuid.getData();
if (bb[0]!='M'||bb[1]!='S'||bb[2]!='I'||bb[3]!='G')
return;
// Version number: not used
// bb[4] bb[5]
final int worldFileInterpretation = bb[6];
// Not used.
// final int chunkNumber = bb[14];
// //
//
// Parsing Chunk
//
// //
final int ckIndex = 16;
final int chunkIndex = bb[ckIndex];
final long chunkLength = Utils.bytes2long(bb, ckIndex+2);
if (chunkIndex!=0 && chunkLength!=48)
return;
// //
// Parsing the Grid to World transformation
// //
final double xScale = Utils.bytes2double(bb,ckIndex+6);
final double xRotation = Utils.bytes2double(bb,ckIndex+14);
final double yRotation = Utils.bytes2double(bb,ckIndex+22);
final double yScale = Utils.bytes2double(bb,ckIndex+30);
final double xUpperLeft = Utils.bytes2double(bb,ckIndex+38);
final double yUpperLeft = Utils.bytes2double(bb,ckIndex+46);
// Not used.
// final boolean footerOk = (bb[ckIndex + 54] == (byte)0xFF) && (bb[ckIndex + 55] == (byte)0x00);
// //
// Setting up the grid to world transformation
// //
final AffineTransform tempTransform = new AffineTransform(
xScale, yRotation, xRotation,
yScale, xUpperLeft, yUpperLeft);
// ////////////////////////////////////////////////////////////////////
//
// Quoting from 3.2.2.1 at:
// http://www.lizardtech.com/support/kb/docs/geotiff_box.txt
//
// "This was instituted with version 1.03.11 (May 15, 2003) to signify that we
// clarified the definition of the georeferencing data and found out that that
// data represents the upper left corner of the upper left pixel, not the
// center as we had thought, so the [world chunk values are] not equal to the
// geotiff data, but is shifted by 0.5*scale to the center of the pixel."
//
// Finally note that:
// If the world chunk is present, these values should override
// the corresponding values in the GeoTIFF box.
//
// ////////////////////////////////////////////////////////////////////
if (worldFileInterpretation == WORLD_FILE_INTERPRETATION_PIXEL_CORNER){
AffineTransform transform = (AffineTransform) ProjectiveTransform.create(tempTransform);
final double tr = -PixelTranslation.getPixelTranslation(PixelInCell.CELL_CORNER);
transform.translate(tr, tr);
this.raster2Model = ProjectiveTransform.create(transform);
} else {
this.raster2Model = ProjectiveTransform.create(tempTransform);
tempTransform.translate(-0.5, -0.5);
}
try {
final GeneralEnvelope envelope = CRS.transform(ProjectiveTransform.create(tempTransform),
new GeneralEnvelope(nativeGridRange));
envelope.setCoordinateReferenceSystem(crs);
this.nativeEnvelope = envelope;
} catch (TransformException e) {
if (LOGGER.isLoggable(Level.FINE))
LOGGER.log(Level.FINE,"Unable to parse CRS from underlying TIFF", e);
} catch (UnsupportedOperationException e) {
if (LOGGER.isLoggable(Level.FINE))
LOGGER.log(Level.FINE,"Unable to parse CRS from underlying TIFF due to an unsupported CRS", e);
}
}
/**
* Get the degenerate GeoTIFF to obtain the related CoordinateReferenceSystem tags
* @param uuid
* @throws IOException
*/
private void getGeoJP2(final UUIDBoxMetadataNode uuid) throws IOException {
CoordinateReferenceSystem coordinateReferenceSystem = null;
final ByteArrayInputStream inputStream = new ByteArrayInputStream(uuid.getData());
final TIFFImageReader tiffreader = (TIFFImageReader) new TIFFImageReaderSpi().createReaderInstance();
tiffreader.setInput(ImageIO.createImageInputStream(inputStream));
final IIOMetadata tiffmetadata = tiffreader.getImageMetadata(0);
try {
final GeoTiffIIOMetadataDecoder metadataDecoder = new GeoTiffIIOMetadataDecoder(tiffmetadata);
final GeoTiffMetadata2CRSAdapter adapter = new GeoTiffMetadata2CRSAdapter(null);
coordinateReferenceSystem = adapter.createCoordinateSystem(metadataDecoder);
if (coordinateReferenceSystem != null) {
if (this.crs == null)
this.crs = coordinateReferenceSystem;
}
if (this.raster2Model == null){
this.raster2Model = adapter.getRasterToModel(metadataDecoder);
final AffineTransform tempTransform = new AffineTransform((AffineTransform) raster2Model);
tempTransform.translate(-0.5, -0.5);
final GeneralEnvelope envelope = CRS.transform(ProjectiveTransform.create(tempTransform),
new GeneralEnvelope(nativeGridRange));
envelope.setCoordinateReferenceSystem(crs);
setCoverageEnvelope(envelope);
}
} catch (FactoryException e) {
if (LOGGER.isLoggable(Level.FINE))
LOGGER.log(Level.FINE,
"Unable to parse CRS from underlying TIFF", e);
coordinateReferenceSystem = null;
} catch (TransformException e) {
if (LOGGER.isLoggable(Level.FINE))
LOGGER.log(Level.FINE,
"Unable to parse CRS from underlying TIFF", e);
coordinateReferenceSystem = null;
} catch (UnsupportedOperationException e) {
if (LOGGER.isLoggable(Level.FINE))
LOGGER.log(Level.FINE,
"Unable to parse CRS from underlying TIFF", e);
coordinateReferenceSystem = null;
} catch (IllegalArgumentException e) {
if (LOGGER.isLoggable(Level.FINE))
LOGGER.log(Level.FINE,
"Unable to parse CRS from underlying TIFF", e);
coordinateReferenceSystem = null;
} finally {
if (inputStream != null)
try {
inputStream.close();
} catch (IOException ioe) {
// Eat exception.
}
}
}
/**
* Number of coverages for this reader is 1
*
* @return the number of coverages for this reader.
*/
@Override
public int getGridCoverageCount() {
return 1;
}
/**
* Releases resources held by this reader.
*
*/
@Override
public synchronized void dispose() {
super.dispose();
rasterManager.dispose();
}
/**
* The source {@link URL}
*/
URL sourceURL;
boolean expandMe;
private RasterManager rasterManager;
private String parentPath;
/**
* Constructor.
*
* @param source
* The source object.
* @throws IOException
* @throws UnsupportedEncodingException
*
*/
public JP2KReader(Object source, Hints uHints) throws IOException {
// //
//
// managing hints
//
// //
if (this.hints == null)
this.hints= new Hints();
if (uHints != null) {
this.hints.add(uHints);
}
this.coverageFactory= CoverageFactoryFinder.getGridCoverageFactory(this.hints);
// /////////////////////////////////////////////////////////////////////
//
// Check source
//
// /////////////////////////////////////////////////////////////////////
if (source == null) {
final IOException ex = new IOException(
"JP2KReader:No source set to read this coverage.");
if (LOGGER.isLoggable(Level.WARNING))
LOGGER.log(Level.WARNING, ex.getLocalizedMessage(), ex);
throw new DataSourceException(ex);
}
this.source = source;
this.sourceURL=Utils.checkSource(source);
if(this.sourceURL==null)
throw new DataSourceException("This plugin accepts only File, URL and String pointing to a file");
final File inputFile = DataUtilities.urlToFile(sourceURL);
if (inputFile == null)
throw new DataSourceException("Unable to find a file for the provided source");
parentPath = inputFile.getParent();
ImageReader reader = null;
final ImageInputStream stream = ImageIO.createImageInputStream(inputFile);
if (cachedSPI == null){
reader = Utils.getReader(stream);
if(reader != null)
cachedSPI = reader.getOriginatingProvider();
}
if (reader == null)
throw new DataSourceException("No reader found for that source " + sourceURL);
reader.setInput(stream);
coverageName = inputFile.getName();
final int dotIndex = coverageName.lastIndexOf(".");
coverageName = (dotIndex == -1) ? coverageName : coverageName
.substring(0, dotIndex);
// //
//
// get the crs if able to
//
// //
final Object tempCRS = this.hints.get(Hints.DEFAULT_COORDINATE_REFERENCE_SYSTEM);
if (tempCRS != null) {
this.crs = (CoordinateReferenceSystem) tempCRS;
LOGGER.log(Level.WARNING, new StringBuffer(
"Using forced coordinate reference system ").append(
crs.toWKT()).toString());
} else {
setCoverageProperties(reader);
if (crs == null) {
// use the default crs
crs = AbstractGridFormat.getDefaultCRS();
LOGGER.log(Level.WARNING,"Unable to find a CRS for this coverage, using a default one: "+crs.toWKT());
}
}
setResolutionInfo(reader);
reader.dispose();
// creating the raster manager
rasterManager = new RasterManager(this);
}
/**
* @see org.opengis.coverage.grid.GridCoverageReader#getFormat()
*/
public Format getFormat() {
return new JP2KFormat();
}
/**
* @see org.opengis.coverage.grid.GridCoverageReader#read(org.opengis.parameter.GeneralParameterValue[])
*/
public GridCoverage2D read(GeneralParameterValue[] params) throws IOException {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Reading image from " + sourceURL.toString());
LOGGER.fine(new StringBuffer("Highest res ").append(highestRes[0])
.append(" ").append(highestRes[1]).toString());
}
final Collection<GridCoverage2D> response = rasterManager.read(params);
if(response.isEmpty())
return null;
else
return response.iterator().next();
}
/**
* Package private accessor for {@link Hints}.
*
* @return this {@link Hints} used by this reader.
*/
Hints getHints(){
return super.hints;
}
/**
* Package private accessor for the highest resolution values.
*
* @return the highest resolution values.
*/
double[] getHighestRes(){
return super.highestRes;
}
double[][] getOverviewsResolution(){
return super.overViewResolutions;
}
int getNumberOfOverviews(){
return super.numOverviews;
}
/** Package scope grid to world transformation accessor */
MathTransform getRaster2Model() {
return raster2Model;
}
/**
* Let us retrieve the {@link GridCoverageFactory} that we want to use.
*
* @return
* retrieves the {@link GridCoverageFactory} that we want to use.
*/
GridCoverageFactory getGridCoverageFactory(){
return coverageFactory;
}
String getName() {
return super.coverageName;
}
/**
* Gets the coordinate reference system that will be associated to the
* {@link GridCoverage} by looking for a related PRJ.
* @throws UnsupportedEncodingException
*/
protected void parsePRJFile() throws UnsupportedEncodingException {
String prjPath = new StringBuilder(parentPath).append(SEPARATOR)
.append(coverageName).append(".prj").toString();
// does it exist?
final File prjFile = new File(prjPath);
if (prjFile.exists()) {
// it exists then we have top read it
PrjFileReader projReader = null;
FileInputStream instream=null;
try {
instream=new FileInputStream(prjFile);
final FileChannel channel = instream.getChannel();
projReader = new PrjFileReader(channel);
crs = projReader.getCoordinateReferenceSystem();
} catch (FileNotFoundException e) {
// warn about the error but proceed, it is not fatal
// we have at least the default crs to use
LOGGER.log(Level.INFO, e.getLocalizedMessage(), e);
} catch (IOException e) {
// warn about the error but proceed, it is not fatal
// we have at least the default crs to use
LOGGER.log(Level.INFO, e.getLocalizedMessage(), e);
} catch (FactoryException e) {
// warn about the error but proceed, it is not fatal
// we have at least the default crs to use
LOGGER.log(Level.INFO, e.getLocalizedMessage(), e);
} finally {
if (projReader != null)
try {
projReader.close();
} catch (IOException e) {
// warn about the error but proceed, it is not fatal
// we have at least the default crs to use
LOGGER .log(Level.FINE, e.getLocalizedMessage(),e);
}
if (instream != null)
try {
instream.close();
} catch (IOException e) {
// warn about the error but proceed, it is not fatal
// we have at least the default crs to use
LOGGER.log(Level.FINE, e.getLocalizedMessage(),e);
}
}
}
}
/**
* Checks whether a world file is associated with the data source. If found,
* set a proper envelope.
*
* @throws IllegalStateException
* @throws IOException
*/
protected void parseWorldFile() throws IOException {
final String worldFilePath = new StringBuffer(this.parentPath).append(
SEPARATOR).append(coverageName).toString();
File file2Parse = null;
boolean worldFileExists = false;
// //
//
// Check for a world file with the format specific extension
//
// //
file2Parse = new File(worldFilePath + ".j2w");
worldFileExists = file2Parse.exists();
// //
//
// Check for a world file with the default extension
//
// //
if (!worldFileExists) {
file2Parse = new File(worldFilePath + ".wld");
worldFileExists = file2Parse.exists();
}
if (worldFileExists) {
final WorldFileReader reader = new WorldFileReader(file2Parse);
raster2Model = reader.getTransform();
// //
//
// In case we read from a real world file we have together the
// envelope. World file transformation assumes to work in the
// CELL_CENTER condition
//
// //
MathTransform tempTransform = PixelTranslation.translate(raster2Model, PixelInCell.CELL_CENTER, PixelInCell.CELL_CORNER);
try {
final Envelope gridRange = new GeneralEnvelope(nativeGridRange);
final GeneralEnvelope coverageEnvelope = CRS.transform(
tempTransform, gridRange);
nativeEnvelope = coverageEnvelope;
} catch (TransformException e) {
if (LOGGER.isLoggable(Level.WARNING)) {
LOGGER.log(Level.WARNING, e.getLocalizedMessage(), e);
}
} catch (IllegalStateException e) {
if (LOGGER.isLoggable(Level.WARNING)) {
LOGGER.log(Level.WARNING, e.getLocalizedMessage(), e);
}
}
}
}
/**
* Gets resolution information about the coverage itself.
*
* @param reader
* an {@link ImageReader} to use for getting the resolution
* information.
* @throws IOException
* @throws TransformException
* @throws DataSourceException
*/
private void setResolutionInfo(ImageReader reader) throws IOException {
// //
//
// get the dimension of the high resolution image and compute the
// resolution
//
// //
final Rectangle originalDim = new Rectangle(0, 0, reader.getWidth(0),
reader.getHeight(0));
if (nativeGridRange == null) {
setCoverageGridRange(new GridEnvelope2D(originalDim));
}
// //
//
// setting the higher resolution available for this coverage
//
// //
highestRes = CoverageUtilities
.getResolution((AffineTransform) raster2Model);
if (LOGGER.isLoggable(Level.FINE))
LOGGER.fine(new StringBuffer("Highest Resolution = [").append(
highestRes[0]).append(",").append(highestRes[1])
.toString());
numOverviews = 0;
overViewResolutions = numOverviews >= 1 ? new double[numOverviews][2]: null;
}
}