/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2015, 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.coverage.grid.io.imageio;
import java.awt.Rectangle;
import java.awt.geom.AffineTransform;
import java.awt.image.RenderedImage;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.HashSet;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.spi.ImageInputStreamSpi;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream;
import javax.media.jai.ROI;
import org.geotools.data.DataUtilities;
import org.geotools.image.ImageWorker;
import org.geotools.image.io.ImageIOExt;
import it.geosolutions.imageio.maskband.DatasetLayout;
/**
* Helper class used for handling Internal/External overviews and masks for a File
*
* @author Nicola Lagomarsini GeoSolutions
*/
public class MaskOverviewProvider {
private final static Logger LOGGER = org.geotools.util.logging.Logging
.getLogger(MaskOverviewProvider.class.toString());
public static final String OVR_EXTENSION = ".ovr";
private ImageReaderSpi readerSpi;
private ImageReaderSpi overviewReaderSpi;
private ImageInputStreamSpi streamSpi;
private ImageInputStreamSpi overviewStreamSpi;
private DatasetLayout layout;
private URL fileURL;
private URL ovrURL;
private int numOverviews;
private final boolean hasDatasetLayout;
private int numInternalOverviews;
private int numExternalOverviews;
private int numInternalMasks;
private int numExternalMasks;
private int numExternalMasksOverviews;
private boolean hasExternalMasks;
private boolean hasExternalMasksOverviews;
private URL maskURL;
private ImageInputStreamSpi maskStreamSpi;
private ImageReaderSpi maskReaderSpi;
private URL maskOvrURL;
private ImageInputStreamSpi maskOvrStreamSpi;
private ImageReaderSpi maskOvrReaderSpi;
public MaskOverviewProvider(DatasetLayout layout, File inputFile) throws IOException {
this(layout, inputFile, (ImageReaderSpi) null);
}
public MaskOverviewProvider(DatasetLayout layout, File inputFile, ImageReaderSpi suggestedSPI)
throws IOException {
this (layout, inputFile, new SpiHelper(inputFile, suggestedSPI));
}
public MaskOverviewProvider(DatasetLayout layout, File inputFile, SpiHelper spiProvider) throws IOException {
ImageReaderSpi suggestedSPI = spiProvider.getSuggestedSpi();
readerSpi = spiProvider.getReaderSpi();
streamSpi = spiProvider.getStreamSpi();
this.fileURL = spiProvider.getFileURL();
this.layout = layout;
// Handling Overviews
File overviewFile = new File(inputFile.getAbsolutePath() + OVR_EXTENSION);
hasDatasetLayout = layout != null;
if (hasDatasetLayout && layout.getExternalOverviews() != null) {
overviewFile = layout.getExternalOverviews();
}
if (overviewFile.exists() && overviewFile.canRead()) {
// Creating overview file URL
ovrURL = DataUtilities.fileToURL(overviewFile);
// Creating cached SPIs
overviewStreamSpi = getInputStreamSPIFromURL(ovrURL);
ImageInputStream ovrStream = null;
try {
ovrStream = overviewStreamSpi.createInputStreamInstance(ovrURL,
ImageIO.getUseCache(), ImageIO.getCacheDirectory());
overviewReaderSpi = getReaderSpiFromStream(null, ovrStream);
} catch (Exception e) {
if (LOGGER.isLoggable(Level.WARNING)) {
LOGGER.log(
Level.WARNING,
"Unable to create a Reader for File: "
+ overviewFile.getCanonicalPath(), e);
}
throw new IllegalArgumentException(e);
} finally {
if (ovrStream != null) {
try {
ovrStream.close();
} catch (Exception e) {
if (LOGGER.isLoggable(Level.SEVERE)) {
LOGGER.log(Level.SEVERE, e.getMessage(), e);
}
}
}
}
} else {
// No Overview file so we fall back to the original file spis
overviewStreamSpi = streamSpi;
overviewReaderSpi = readerSpi;
}
// Getting number of Overviews
int numOverviews = 0;
if (hasDatasetLayout) {
numInternalOverviews = layout.getNumInternalOverviews();
// layout.getNumExternalOverviews() may return -1 when no external file is present
numExternalOverviews = layout.getNumExternalOverviews() > 0 ? layout
.getNumExternalOverviews() : 0;
numOverviews = numInternalOverviews + numExternalOverviews;
} else if (!spiProvider.isMultidim()){
// Reading image number
ImageInputStream imageStream = null;
ImageReader reader = null;
try {
// Creating stream
imageStream = streamSpi.createInputStreamInstance(fileURL, ImageIO.getUseCache(),
ImageIO.getCacheDirectory());
// Creating reader
reader = readerSpi.createReaderInstance();
// Setting input
reader.setInput(imageStream, false, false);
// Getting number of images
numOverviews = reader.getNumImages(true) - 1;
// Setting numInternalOverviews
numInternalOverviews = numOverviews;
} catch (Exception e) {
if (LOGGER.isLoggable(Level.WARNING)) {
LOGGER.log(Level.WARNING,
"Unable to create a Reader for File: " + inputFile.getCanonicalPath(),
e);
}
throw new IllegalArgumentException(e);
} finally {
if (imageStream != null) {
try {
imageStream.close();
} catch (Exception e) {
if (LOGGER.isLoggable(Level.SEVERE)) {
LOGGER.log(Level.SEVERE, e.getMessage(), e);
}
} finally {
if (reader != null) {
reader.dispose();
}
}
}
}
}
if (numOverviews < 0) {
numOverviews = 0;
}
// Setting overviews Number
this.numOverviews = numOverviews;
// Mask Management
if (layout != null) {
numInternalMasks = layout.getNumInternalMasks();
numExternalMasks = layout.getNumExternalMasks() > 0 ? layout.getNumExternalMasks() : 0;
numExternalMasksOverviews = layout.getNumExternalMaskOverviews() > 0 ? layout
.getNumExternalMaskOverviews() : 0;
hasExternalMasks = numExternalMasks > 0;
hasExternalMasksOverviews = hasExternalMasks && numExternalMasksOverviews > 0;
if (hasExternalMasks) {
// Mask URL
maskURL = DataUtilities.fileToURL(layout.getExternalMasks());
// Creating cached SPIs
maskStreamSpi = getInputStreamSPIFromURL(maskURL);
ImageInputStream maskStream = null;
try {
maskStream = maskStreamSpi.createInputStreamInstance(maskURL,
ImageIO.getUseCache(), ImageIO.getCacheDirectory());
maskReaderSpi = getReaderSpiFromStream(suggestedSPI, maskStream);
} catch (Exception e) {
if (LOGGER.isLoggable(Level.WARNING)) {
LOGGER.log(Level.WARNING, "Unable to create a Reader for File: " + maskURL,
e);
}
throw new IllegalArgumentException(e);
} finally {
if (maskStream != null) {
try {
maskStream.close();
} catch (Exception e) {
if (LOGGER.isLoggable(Level.SEVERE)) {
LOGGER.log(Level.SEVERE, e.getMessage(), e);
}
}
}
}
// Handling external mask overviews
if (hasExternalMasksOverviews) {
// Mask URL
maskOvrURL = DataUtilities.fileToURL(layout.getExternalMaskOverviews());
// Creating cached SPIs
maskOvrStreamSpi = getInputStreamSPIFromURL(maskOvrURL);
ImageInputStream maskOvrStream = null;
try {
maskOvrStream = maskOvrStreamSpi.createInputStreamInstance(maskOvrURL,
ImageIO.getUseCache(), ImageIO.getCacheDirectory());
maskOvrReaderSpi = getReaderSpiFromStream(suggestedSPI, maskOvrStream);
} catch (Exception e) {
if (LOGGER.isLoggable(Level.WARNING)) {
LOGGER.log(Level.WARNING, "Unable to create a Reader for File: "
+ maskOvrURL, e);
}
throw new IllegalArgumentException(e);
} finally {
if (maskOvrStream != null) {
try {
maskOvrStream.close();
} catch (Exception e) {
if (LOGGER.isLoggable(Level.SEVERE)) {
LOGGER.log(Level.SEVERE, e.getMessage(), e);
}
}
}
}
} else {
// No Mask Overview file so we fall back to the original mask spis
maskOvrStreamSpi = maskStreamSpi;
maskOvrReaderSpi = maskReaderSpi;
}
} else {
// No Mask file so we fall back to the original file spis
maskStreamSpi = streamSpi;
maskReaderSpi = readerSpi;
}
}
}
/**
* Returns the external/internal overview image index based on the initial imageindex value
*/
public int getOverviewIndex(int imageIndex) {
if (numExternalOverviews > 0 && imageIndex >= (numInternalOverviews + 1)) {
return imageIndex - numInternalOverviews - 1;
}
if (layout != null) {
return layout.getInternalOverviewImageIndex(imageIndex);
}
return imageIndex;
}
/**
* Returns a new {@link MaskInfo} instance containing all the parameters to set for accessing the desired image index
*/
public MaskInfo getMaskInfo(int imageIndex, Rectangle imageBounds,
ImageReadParam originalParams) {
MaskInfo info = null;
if (numInternalMasks + numExternalMasks > 0) {
// Create a new MaskInfo instance
info = new MaskInfo();
// Parameter definiton
ImageReadParam readParam = new ImageReadParam();
readParam.setSourceSubsampling(originalParams.getSourceXSubsampling(),
originalParams.getSourceYSubsampling(), originalParams.getSubsamplingXOffset(),
originalParams.getSubsamplingYOffset());
Rectangle sourceRegion = imageBounds;
if (originalParams.getSourceRegion() != null) {
sourceRegion = originalParams.getSourceRegion();
}
readParam.setSourceRegion(sourceRegion);
info.readParameters = readParam;
// Checks on the overviews
if (imageIndex > 0) {
// Check if the ImageChoice is contained inside internal or external masks
if (imageIndex < numInternalMasks) {
info.file = DataUtilities.urlToFile(fileURL);
info.readerSpi = readerSpi;
info.streamSpi = streamSpi;
info.index = imageIndex != 0 ? layout.getInternalMaskImageIndex(imageIndex) - 1
: layout.getInternalMaskImageIndex(imageIndex);
} else if (hasExternalMasks) {
if (imageIndex < numExternalMasks) {
info.file = DataUtilities.urlToFile(maskURL);
info.readerSpi = maskReaderSpi;
info.streamSpi = maskStreamSpi;
info.index = imageIndex;
} else if (imageIndex < numExternalMasks + numExternalMasksOverviews) {
info.file = DataUtilities.urlToFile(maskOvrURL);
info.readerSpi = maskOvrReaderSpi;
info.streamSpi = maskOvrStreamSpi;
info.index = imageIndex - numExternalMasks;
} else {
// Read a bigger image
if (numExternalMasksOverviews > 0) {
// reading External Mask External Overviews
info.file = DataUtilities.urlToFile(maskOvrURL);
info.readerSpi = maskOvrReaderSpi;
info.streamSpi = maskOvrStreamSpi;
info.index = numExternalMasksOverviews - 1;
} else {
// reading External Mask Overviews
info.file = DataUtilities.urlToFile(maskURL);
info.readerSpi = maskReaderSpi;
info.streamSpi = maskStreamSpi;
info.index = numExternalMasks - 1;
}
}
} else {
// Reading Internal Mask Overview
info.file = DataUtilities.urlToFile(fileURL);
info.readerSpi = readerSpi;
info.streamSpi = streamSpi;
info.index = numInternalMasks - 1;
}
// Checks on the native image data
} else if (imageIndex == 0) {
// Checking for external Masks
if (numInternalMasks == 0 && hasExternalMasks) {
// reading External Mask Overviews
info.file = DataUtilities.urlToFile(maskURL);
info.readerSpi = maskReaderSpi;
info.streamSpi = maskStreamSpi;
info.index = 0;
// Check for internal Masks
} else if (numInternalMasks > 0) {
// Reading Internal Mask Overview
info.file = DataUtilities.urlToFile(fileURL);
info.readerSpi = readerSpi;
info.streamSpi = streamSpi;
info.index = layout.getInternalMaskImageIndex(0);
}
}
}
return info;
}
/**
* Returns true if there is a mask at the same resolution of the requested one
*/
public boolean hasMaskIndexForOverview(int imageIndex) {
// Checks on the overviews
if (imageIndex > 0) {
// Check if the ImageChoice is contained inside internal or external masks
if (imageIndex < numInternalMasks) {
return true;
} else if (hasExternalMasks
&& imageIndex <= (numExternalMasks + numExternalMasksOverviews - 1)) {
return true;
}
// Checks on the 0 level
} else if (imageIndex == 0 && numInternalMasks > 0 || hasExternalMasks) {
return true;
}
return false;
}
/**
* Returns true if the defined index is related to an external image overview index
*/
public boolean isExternalOverview(int imageIndex) {
if (numExternalOverviews <= 0) {
return false;
}
return imageIndex > numInternalOverviews;
}
/**
* Returns true if the defined index is related to an external image mask index
*/
public boolean isExternalMask(int imageIndex) {
if (numExternalMasks <= 0) {
return false;
}
return hasExternalMasks && imageIndex > (numInternalMasks > 0 ? numInternalMasks + 1 : 0);
}
/**
* Returns true if the defined index is related to an external image mask overview index
*/
public boolean isExternalMaskOverviews(int imageIndex) {
if (numExternalMasksOverviews <= 0) {
return false;
}
return isExternalMask(imageIndex)
&& hasExternalMasksOverviews
&& imageIndex > (numInternalMasks > 0 ? numInternalMasks + numExternalMasks + 2
: numExternalMasks + 1);
}
public boolean hasExternalMasks() {
return hasExternalMasks;
}
public boolean hasExternalMasksOverviews() {
return hasExternalMasksOverviews;
}
/**
* Returns a double[][] containing the resolutions for all the overviews
*/
public double[][] getOverviewResolutions(double span0, double span1) {
double[][] overviewsResolution = null;
if (numOverviews > 0) {
ImageInputStream stream = null;
ImageInputStream streamOvr = null;
ImageReader reader = null;
ImageReader readerOvr = null;
try {
// Instantiating Stream
stream = getInputStreamSpi().createInputStreamInstance(fileURL,
ImageIO.getUseCache(), ImageIO.getCacheDirectory());
reader = getImageReaderSpi().createReaderInstance();
reader.setInput(stream, false, false);
if (ovrURL != null) {
streamOvr = getExternalOverviewInputStreamSpi().createInputStreamInstance(
ovrURL, ImageIO.getUseCache(), ImageIO.getCacheDirectory());
readerOvr = getExternalOverviewReaderSpi().createReaderInstance();
readerOvr.setInput(streamOvr, false, false);
}
overviewsResolution = new double[numOverviews][2];
// Populating overviews
for (int i = 0; i < numOverviews; i++) {
// Handling internal and external overviews
if (numExternalOverviews > 0 && i >= numInternalOverviews) {
int index = i - numInternalOverviews;
overviewsResolution[i][0] = span0 / readerOvr.getWidth(index);
overviewsResolution[i][1] = span1 / readerOvr.getHeight(index);
} else {
int index = hasDatasetLayout ? layout.getInternalOverviewImageIndex(i + 1)
: i + 1;
overviewsResolution[i][0] = span0 / reader.getWidth(index);
overviewsResolution[i][1] = span1 / reader.getHeight(index);
}
}
} catch (Exception e) {
if (LOGGER.isLoggable(Level.WARNING)) {
LOGGER.log(Level.WARNING, "Unable to create a Reader for File: " + fileURL, e);
}
throw new IllegalArgumentException(e);
} finally {
// Closing stream and readers
if (stream != null) {
try {
stream.close();
} catch (Exception e) {
if (LOGGER.isLoggable(Level.SEVERE)) {
LOGGER.log(Level.SEVERE, e.getMessage(), e);
}
} finally {
if (reader != null) {
reader.dispose();
}
}
}
if (streamOvr != null) {
try {
streamOvr.close();
} catch (Exception e) {
if (LOGGER.isLoggable(Level.SEVERE)) {
LOGGER.log(Level.SEVERE, e.getMessage(), e);
}
} finally {
if (readerOvr != null) {
readerOvr.dispose();
}
}
}
}
}
return overviewsResolution;
}
public ImageReaderSpi getExternalOverviewReaderSpi() {
return overviewReaderSpi;
}
public ImageReaderSpi getImageReaderSpi() {
return readerSpi;
}
public ImageInputStreamSpi getExternalOverviewInputStreamSpi() {
return overviewStreamSpi;
}
public ImageInputStreamSpi getInputStreamSpi() {
return streamSpi;
}
public ImageInputStreamSpi getMaskStreamSpi() {
return maskStreamSpi;
}
public ImageReaderSpi getMaskReaderSpi() {
return maskReaderSpi;
}
public ImageInputStreamSpi getMaskOvrStreamSpi() {
return maskOvrStreamSpi;
}
public ImageReaderSpi getMaskOvrReaderSpi() {
return maskOvrReaderSpi;
}
public DatasetLayout getLayout() {
return layout;
}
public int getNumOverviews() {
return numOverviews;
}
public int getNumInternalOverviews() {
return numInternalOverviews;
}
public int getNumExternalOverviews() {
return numExternalOverviews;
}
public int getNumInternalMasks() {
return numInternalMasks;
}
public int getNumExternalMasks() {
return numExternalMasks;
}
public int getNumExternalMasksOverviews() {
return numExternalMasksOverviews;
}
public URL getFileURL() {
return fileURL;
}
public URL getOvrURL() {
return ovrURL;
}
public URL getMaskURL() {
return maskURL;
}
public URL getMaskOvrURL() {
return maskOvrURL;
}
/**
* Returns an {@link ImageInputStreamSpi} instance for the input {@link URL}
*/
public static ImageInputStreamSpi getInputStreamSPIFromURL(URL fileURL) throws IOException {
ImageInputStreamSpi streamSPI = ImageIOExt.getImageInputStreamSPI(fileURL, true);
if (streamSPI == null) {
final File file = DataUtilities.urlToFile(fileURL);
if (file != null) {
if (LOGGER.isLoggable(Level.WARNING)) {
LOGGER.log(Level.WARNING, file.getCanonicalPath());
}
}
throw new IllegalArgumentException(
"Unable to get an input stream for the provided source " + fileURL.toString());
}
return streamSPI;
}
/**
* Returns an {@link ImageReaderSpi} instance for the input {@link ImageInputStream} and the optional suggested spi.
*/
public static ImageReaderSpi getReaderSpiFromStream(ImageReaderSpi suggestedSPI,
ImageInputStream inStream) throws IOException {
ImageReaderSpi readerSPI = null;
// get a reader and try to use the suggested SPI first
inStream.mark();
if (suggestedSPI != null && suggestedSPI.canDecodeInput(inStream)) {
readerSPI = suggestedSPI;
inStream.reset();
} else {
inStream.mark();
ImageReader reader = ImageIOExt.getImageioReader(inStream);
if (reader != null)
readerSPI = reader.getOriginatingProvider();
inStream.reset();
}
return readerSPI;
}
/**
* Returns a {@link ROI} object based on the input {@link RenderedImage} representing ROI
*/
public static ROI scaleROI(RenderedImage roiRaster, Rectangle bounds) {
if (roiRaster == null) {
return null;
}
int x = bounds.x;
int y = bounds.y;
int w = bounds.width;
int h = bounds.height;
// Scale factors for input data
final double scaleX = w / (1.0 * roiRaster.getWidth());
final double scaleY = h / (1.0 * roiRaster.getHeight());
AffineTransform tr = AffineTransform.getScaleInstance(scaleX, scaleY);
// Translation Factors
final int transX = x;
final int transY = y;
tr.concatenate(AffineTransform.getTranslateInstance(transX, transY));
// Log the Scale/Translate operation
if (!tr.isIdentity()) {
LOGGER.fine("Scaling ROI");
}
// Input Mask is scaled to the image size, rescaled to Bytes and then used as ROI
return new ImageWorker(roiRaster).affine(tr, null, null).binarize(1).getImageAsROI();
}
/**
* Helper class used for storing information to use for read the mask data.
*
* @author Nicola Lagomarsini GeoSolutions
*/
public static class MaskInfo {
public File file;
public int index;
public ImageReadParam readParameters;
public ImageReaderSpi readerSpi;
public ImageInputStreamSpi streamSpi;
}
/**
* Helper class containing previous code used to get
* streamSPI and readerSPI for an input file.
*/
public static class SpiHelper {
private final static Set<String> MULTIDIM_SERVICE_PROVIDERS;
static {
MULTIDIM_SERVICE_PROVIDERS = new HashSet<String>();
MULTIDIM_SERVICE_PROVIDERS.add("org.geotools.imageio.netcdf.NetCDFImageReaderSpi");
}
private ImageReaderSpi suggestedSpi;
private ImageReaderSpi readerSpi;
private ImageInputStreamSpi streamSpi;
private URL fileURL;
/**
* Reporting whether the SPI is for a multidim reader or not.
* GRIB/NetCDF and other multidim format doesn't have overviews
*/
private boolean isMultidim;
public SpiHelper(File inputFile, ImageReaderSpi suggestedSPI) throws IOException {
this.suggestedSpi = suggestedSPI;
this.fileURL = DataUtilities.fileToURL(inputFile);
// Creating cached SPIs
streamSpi = getInputStreamSPIFromURL(fileURL);
ImageInputStream stream = null;
try {
stream = streamSpi.createInputStreamInstance(fileURL, ImageIO.getUseCache(),
ImageIO.getCacheDirectory());
readerSpi = getReaderSpiFromStream(suggestedSPI, stream);
isMultidim = readerSpi != null && MULTIDIM_SERVICE_PROVIDERS.contains(readerSpi.getClass().getName());
} catch (Exception e) {
if (LOGGER.isLoggable(Level.WARNING)) {
LOGGER.log(Level.WARNING,
"Unable to create a Reader for File: " + inputFile.getCanonicalPath(), e);
}
throw new IllegalArgumentException(e);
} finally {
if (stream != null) {
try {
stream.close();
} catch (Exception e) {
if (LOGGER.isLoggable(Level.SEVERE)) {
LOGGER.log(Level.SEVERE, e.getMessage(), e);
}
}
}
}
}
public boolean isMultidim() {
return isMultidim;
}
public ImageReaderSpi getReaderSpi() {
return readerSpi;
}
public ImageInputStreamSpi getStreamSpi() {
return streamSpi;
}
public URL getFileURL() {
return fileURL;
}
public ImageReaderSpi getSuggestedSpi() {
return suggestedSpi;
}
}
}