/*
* Geotoolkit - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2016, 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.landsat;
import java.awt.Dimension;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
import java.io.IOException;
import java.nio.file.Path;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CancellationException;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import org.opengis.coverage.grid.GridCoverage;
import org.opengis.coverage.grid.GridEnvelope;
import org.opengis.geometry.Envelope;
import org.opengis.metadata.Metadata;
import org.opengis.parameter.ParameterValueGroup;
import org.opengis.util.FactoryException;
import org.opengis.util.GenericName;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.datum.PixelInCell;
import org.opengis.referencing.operation.MathTransform;
import org.apache.sis.referencing.operation.matrix.Matrices;
import org.apache.sis.referencing.operation.matrix.MatrixSIS;
import org.apache.sis.util.ArgumentChecks;
import org.geotoolkit.coverage.GridSampleDimension;
import org.geotoolkit.coverage.grid.GeneralGridEnvelope;
import org.geotoolkit.coverage.grid.GeneralGridGeometry;
import org.geotoolkit.coverage.grid.GridCoverageBuilder;
import org.geotoolkit.coverage.grid.GridGeometry2D;
import org.geotoolkit.coverage.io.CoverageStoreException;
import org.geotoolkit.coverage.io.GridCoverageReadParam;
import org.geotoolkit.coverage.io.GridCoverageReader;
import org.geotoolkit.coverage.io.ImageCoverageReader;
import org.geotoolkit.image.io.plugin.TiffImageReader;
import org.geotoolkit.internal.referencing.CRSUtilities;
import org.geotoolkit.process.ProcessDescriptor;
import org.geotoolkit.process.ProcessFinder;
import static org.geotoolkit.coverage.landsat.LandsatConstants.*;
import org.geotoolkit.storage.coverage.CoverageReaderHelper;
/**
* Reader to read Landsat datas.
*
* @author Remi Marechal (Geomatys)
* @version 1.0
* @since 1.0
*/
public class LandsatReader extends GridCoverageReader {
static {
/**
* Index arrays which contain index to retrieve appropriates band index to
* build different coverage from Landsat datas.
* Values came from Landsat8 documentation.
*/
final int[] REFLECTIVE_BAND_ID = new int[]{1, 2, 3, 4, 5, 6, 7, 9};
final int[] PANCHROMATIC_BAND_ID = new int[]{8};
final int[] THERMIC_BAND_ID = new int[]{10, 11};
BANDS_INDEX = new int[][]{REFLECTIVE_BAND_ID,
PANCHROMATIC_BAND_ID,
THERMIC_BAND_ID};
}
/**
* Index arrays which contain index to retrieve appropriates band index to
* build different coverage from Landsat datas.
*/
private final static int[][] BANDS_INDEX;
/**
* TiffImageReader SPI used to read images
*/
private static final TiffImageReader.Spi TIFF_SPI = new TiffImageReader.Spi();
/**
* {@link Path} of the directory which contain all band images.
*/
private final Path parenPath;
/**
* Parser to read metadata.
*/
private final LandsatMetadataParser metaParse;
/**
* Array which contains all sample dimension for each read index.
*/
private final List[] gsdLandsat = new List[3];
/**
* Build a Landsat reader from Parent directory path and metadata file path.
*
* @param parentDirectory directory path which contain all Landsat bands images.
* @param metadata path to metadata file.
* @throws IOException if problem during metadatas parser building.
*/
LandsatReader(final Path parentDirectory, final Path metadata) throws IOException {
ArgumentChecks.ensureNonNull("parent directory path", parentDirectory);
ArgumentChecks.ensureNonNull("metadata path", metadata);
this.parenPath = parentDirectory;
this.metaParse = new LandsatMetadataParser(metadata);
}
/**
* Build a Landsat reader from Parent directory path and metadata file path.
*
* @param parentDirectory directory path which contain all Landsat bands images.
* @param metadataParser metadata parser for Landsat8.
* @throws IOException if problem during metadatas parser building.
*/
LandsatReader(final Path parentDirectory, final LandsatMetadataParser metadataParser) throws IOException {
ArgumentChecks.ensureNonNull("parent directory path", parentDirectory);
ArgumentChecks.ensureNonNull("metadata parser", metadataParser);
this.parenPath = parentDirectory;
this.metaParse = metadataParser;
}
/**
* {@inheritDoc }
* Returns ISO19115 metadata filled by Landsat metadatas.
*
* @return ISO19115 metadata filled by Landsat metadatas.
* @throws CoverageStoreException if problem during metadata parsing.
*/
@Override
public Metadata getMetadata() throws CoverageStoreException {
try {
return metaParse.getMetadata(GENERAL_LABEL);
} catch (FactoryException | ParseException ex) {
throw new CoverageStoreException("Landsat coverage reader metadata parsing : ", ex);
}
}
/**
* {@inheritDoc }
*
* @return
* @throws CoverageStoreException
* @throws CancellationException
*/
@Override
public List<? extends GenericName> getCoverageNames() throws CoverageStoreException, CancellationException {
throw new UnsupportedOperationException("Not supported yet.");
}
/**
* {@inheritDoc }
*
* @param index 0, 1 or 2 for respectively ({@link LandsatConstants#REFLECTIVE_LABEL},
* {@link LandsatConstants#PANCHROMATIC_LABEL}, {@link LandsatConstants#THERMAL_LABEL}).
* @return
* @throws CoverageStoreException
* @throws CancellationException
*/
@Override
public GeneralGridGeometry getGridGeometry(int index) throws CoverageStoreException, CancellationException {
final String coverageName;
switch (index) {
case 0 : {
//-- Reflective case RGB band 1, 2, 3, 4, 5, 6, 7, 9
coverageName = REFLECTIVE_LABEL;
break;
}
case 1 : {
//-- Panchromatic case one sample band 8
coverageName = PANCHROMATIC_LABEL;
break;
}
case 2 : {
//-- Thermic case band 10 and 11
coverageName = THERMAL_LABEL;
break;
}
default : throw new CoverageStoreException("Current index "+index+" is not known, available index are : "
+ "0 for reflective, 1 for panchromatic and 2 for Thermic");
}
final GridEnvelope gridExtent;
final MathTransform gridToCRS;
final CoordinateReferenceSystem crs;
try {
gridExtent = metaParse.getGridExtent(coverageName);
gridToCRS = metaParse.getGridToCRS(coverageName);
crs = metaParse.getCRS();
} catch (Exception ex) {
throw new CoverageStoreException(ex);
}
return new GridGeometry2D(gridExtent, PixelInCell.CELL_CORNER, gridToCRS, crs, null);
}
/**
* {@inheritDoc }
*
* @param index 0, 1 or 2 for respectively ({@link LandsatConstants#REFLECTIVE_LABEL},
* {@link LandsatConstants#PANCHROMATIC_LABEL}, {@link LandsatConstants#THERMAL_LABEL}).
* @return
* @throws CoverageStoreException
* @throws CancellationException
*/
@Override
public List<GridSampleDimension> getSampleDimensions(int index) throws CoverageStoreException, CancellationException {
if (gsdLandsat[index] != null)
return gsdLandsat[index];
final int[] bandId = getBandsIndex(index);
final List<GridSampleDimension> gList = new ArrayList<>();
for (int i : bandId) {
final String bandName = metaParse.getValue(true, BAND_NAME_LABEL + i);
final Path resolve = parenPath.resolve(bandName);
final ImageCoverageReader imageCoverageReader = new ImageCoverageReader();
try {
final ImageReader tiffReader = TIFF_SPI.createReaderInstance();
tiffReader.setInput(resolve);
imageCoverageReader.setInput(tiffReader);
gList.addAll(imageCoverageReader.getSampleDimensions(0));
} catch (IOException ex) {
throw new CoverageStoreException(ex);
} finally {
imageCoverageReader.dispose();
}
}
gsdLandsat[index] = gList;
return gsdLandsat[index];
}
/**
* {@inheritDoc }
*
* A Landsat 8 data may contain many internal coverage.<br>
*
* Index parameter differenciates this kind of read : <br>
* - 0 to read REFLECTIVE part of Landsat 8 Coverage (8 bands).<br>
* - 1 to read PANCHROMATIC part of Landsat 8 Coverage (1 bands).<br>
* - 2 to read THERMIC part of Landsat 8 Coverage (2 bands).
*
* @param index 0, 1 or 2 for respectively ({@link LandsatConstants#REFLECTIVE_LABEL},
* {@link LandsatConstants#PANCHROMATIC_LABEL}, {@link LandsatConstants#THERMAL_LABEL}).
* @param param
* @return
* @throws CoverageStoreException
* @throws CancellationException
*/
@Override
public GridCoverage read(int index, GridCoverageReadParam param)
throws CoverageStoreException, CancellationException {
//-- get all needed band to build coverage (see Landsat spec)
final int[] bandId = getBandsIndex(index);
//-- create an adapted coverage name
StringBuilder strBuild = new StringBuilder("Landsat8 : ");
switch (index) {
case 0 : {
//-- Reflective case RGB band 1, 2, 3, 4, 5, 6, 7, 9
strBuild.append(REFLECTIVE_LABEL);
break;
}
case 1 : {
//-- Panchromatic case one sample band 8
strBuild.append(PANCHROMATIC_LABEL);
break;
}
case 2 : {
//-- Thermic case band 10 and 11
strBuild.append(THERMAL_LABEL);
break;
}
default : strBuild.append("Unknow type");
}
strBuild.append("-");
strBuild.append(parenPath.getName(parenPath.getNameCount()-1));
try {
final CoverageReaderHelper crh = new CoverageReaderHelper((GridGeometry2D) getGridGeometry(index), param);
final Dimension outImgSize = crh.getOutImgSize();
final Rectangle srcImgBoundary = crh.getSrcImgBoundary();
final GridGeometry2D destGridGeometry = crh.getDestGridGeometry();
final RenderedImage[] bands = new RenderedImage[bandId.length];
int currentCov = 0;
for (int i : bandId) {
//-- get band name
final String bandName = metaParse.getValue(true, BAND_NAME_LABEL + i);
//-- add to coverage name
final Path band = parenPath.resolve(bandName);
//-- reader config
final TiffImageReader tiffReader = (TiffImageReader) TIFF_SPI.createReaderInstance();
tiffReader.setInput(band);
final ImageReadParam readParam = tiffReader.getDefaultReadParam();
readParam.setSourceRegion(srcImgBoundary);
readParam.setSourceSubsampling(Math.max((int) Math.round(srcImgBoundary.getWidth() / outImgSize.getWidth()), 1),
Math.max((int) Math.round(srcImgBoundary.getHeight() / outImgSize.getHeight()), 1), 0, 0);
try {
BufferedImage read = tiffReader.read(0, readParam);
bands[currentCov++] = read;
} finally {
tiffReader.dispose();
}
}
//-- test avec ca
// GridCoverage2D coverageRGBIR = new BandCombineProcess(
// coverageR,coverageG,coverageB,coverageIR).executeNow();
final ProcessDescriptor desc = ProcessFinder.getProcessDescriptor("image", "bandcombine");
assert (desc != null);
final ParameterValueGroup params = desc.getInputDescriptor().createValue();
params.parameter("images").setValue(bands);
final org.geotoolkit.process.Process process = desc.createProcess(params);
final ParameterValueGroup result = process.call();
//check result coverage
final RenderedImage outImage = (RenderedImage) result.parameter("result").getValue();
/*
* After image reading with integer subsampling scales image haven't got expected size.
* Re-build coverage from image and requested envelope
*/
final int originFHA = CRSUtilities.firstHorizontalAxis(destGridGeometry.getCoordinateReferenceSystem());
//-- gridToCRS
final Envelope envelope = destGridGeometry.getEnvelope();
final MathTransform gridToCRS = destGridGeometry.getGridToCRS(PixelInCell.CELL_CORNER);
final int srcDim = gridToCRS.getSourceDimensions();
final int destDim = gridToCRS.getTargetDimensions();
int[] upper = new int[destDim];
Arrays.fill(upper, 1);
upper[originFHA] = outImage.getWidth();
upper[originFHA + 1] = outImage.getHeight();
final GeneralGridEnvelope ggE = new GeneralGridEnvelope(new int[srcDim], upper, false);
final MatrixSIS sisMatrix = Matrices.createDiagonal(destDim + 1, srcDim + 1);
final double scaleX = envelope.getSpan(originFHA) / outImage.getWidth();
final double scaleY = envelope.getSpan(originFHA + 1) / outImage.getHeight();
final double transX = envelope.getMinimum(originFHA);
final double transY = envelope.getMaximum(originFHA + 1);
sisMatrix.setElement(originFHA, originFHA, scaleX);
sisMatrix.setElement(originFHA + 1, originFHA + 1, - scaleY);
sisMatrix.setElement(originFHA, srcDim, transX);
sisMatrix.setElement(originFHA + 1, srcDim, transY);
final GridGeometry2D gridGeometry2D = new GridGeometry2D(ggE, PixelInCell.CELL_CORNER, gridToCRS,
destGridGeometry.getCoordinateReferenceSystem(), null);
//-- new built grid to CRS
//-- build new coverage
final GridCoverageBuilder gridCoverageBuilder = new GridCoverageBuilder();
gridCoverageBuilder.setName(strBuild.toString());
gridCoverageBuilder.setGridGeometry(gridGeometry2D);
gridCoverageBuilder.setRenderedImage(outImage);
List<GridSampleDimension> sampleDimensions = getSampleDimensions(index);
gridCoverageBuilder.setSampleDimensions(sampleDimensions.toArray(new GridSampleDimension[sampleDimensions.size()]));
return gridCoverageBuilder.build();
} catch (Exception ex) {
throw new CoverageStoreException(ex);
}
}
/**
* Returns array index of future aggregated bands from read index.<br>
* Supported values are 0 for REFLECTIVE, 1 for PANCHROMATIC and 2 for THERMIC.
*
* @param readIndex Coverage read index.
* @return array index of future aggregated bands
* @throws CoverageStoreException if index is out of [0; 3] boundary.
*/
private static int[] getBandsIndex(final int readIndex) throws CoverageStoreException {
if (readIndex < 0 || readIndex > 2)
throw new CoverageStoreException("Current index "+readIndex+" is not known, available index are : "
+ "0 for reflective, 1 for panchromatic and 2 for Thermic");
return BANDS_INDEX[readIndex];
}
/**
* Following method in commentary in wainting for tiffImageReader update about ImageReadParam.setSrcRenderSize().
*/
//// /**
//// * {@inheritDoc }
//// *
//// * A Landsat 8 data may contain many internal coverage.<br>
//// *
//// * Index parameter differenciates this kind of read : <br>
//// * - 0 to read REFLECTIVE part of Landsat 8 Coverage (8 bands).<br>
//// * - 1 to read PANCHROMATIC part of Landsat 8 Coverage (1 bands).<br>
//// * - 2 to read THERMIC part of Landsat 8 Coverage (2 bands).
//// *
//// * @param index 0, 1 or 2.
//// * @param param
//// * @return
//// * @throws CoverageStoreException
//// * @throws CancellationException
//// */
//// @Override
//// public GridCoverage read(int index, GridCoverageReadParam param) throws CoverageStoreException, CancellationException {
////
//// final int[] bandId;
//// switch (index) {
//// case 0 : {
//// //-- Reflective case RGB band 1, 2, 3, 4, 5, 6, 7, 9
//// bandId = REFLECTIVE_BAND_ID;
//// break;
//// }
//// case 1 : {
//// //-- Panchromatic case one sample band 8
//// bandId = PANCHROMATIC_BAND_ID;
//// break;
//// }
//// case 2 : {
//// //-- Thermic case band 10 and 11
//// bandId = THERMIC_BAND_ID;
//// break;
//// }
//// default : throw new CoverageStoreException("Current index "+index+" is not known, available index are : "
//// + "0 for reflective, 1 for panchromatic and 2 for Thermic");
//// }
////
//// try {
////
//// final Path[] bands = new Path[bandId.length];
//// int b = 0;
//// //-- build all bands path
//// for (int i : bandId) {
//// final String bandName = metaParse.getValue(true, BAND_NAME_LABEL+i);
//// bands[b++] = parenPath.resolve(bandName);
//// }
////
//// final GridGeometry2D originGridGeometry = (GridGeometry2D) getGridGeometry(index);
////
//// final CoverageReaderHelper crh = new CoverageReaderHelper(originGridGeometry, param);
////
//// //-- srcImgBoundary
//// final Rectangle srcImgBoundary = crh.getSrcImgBoundary();
////
//// //-- out img boundary
//// final Dimension outimgDimension = crh.getOutImgSize();
////
//// //-- destination grid geometry
//// GridGeometry2D destGridGeometry = crh.getDestGridGeometry();
////
//// //-- build appropriate ColorModel and SampleModel
//// final ImageReader reader = XImageIO.getReader(bands[0].toFile(), false, true);
//// SampleType sampTip = SampleType.valueOf(reader.getRawImageType(0).getSampleModel().getDataType());
//// reader.dispose();
////
//// final ColorModel colorModel = ImageUtils.createColorModel(sampTip, bands.length, PhotometricInterpretation.GrayScale, null);
//// final SampleModel sampleModel = ImageUtils.createSampleModel(PlanarConfiguration.Banded, sampTip, outimgDimension.width, outimgDimension.height, bands.length);
////
//// //-- build specifical sentinel Large Image
//// final Landsat8RenderedImage landsatRenderedImage = new Landsat8RenderedImage(srcImgBoundary, outimgDimension,
//// sampleModel, colorModel, bands);
////
//// final List<GridSampleDimension> sampleDimensions = getSampleDimensions(index);
//// final GridCoverageBuilder gcb = new GridCoverageBuilder();
//// gcb.setRenderedImage(landsatRenderedImage);
//// gcb.setGridGeometry(destGridGeometry);
//// gcb.setSampleDimensions(sampleDimensions.toArray(new GridSampleDimension[sampleDimensions.size()]));
////
//// return gcb.getGridCoverage2D();
//// } catch (TransformException |IOException ex) {
//// throw new CoverageStoreException(ex);
//// }
//// }
}