/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2002-2009, 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.arcsde.raster.info; import static org.geotools.arcsde.raster.info.RasterCellType.TYPE_16BIT_S; import static org.geotools.arcsde.raster.info.RasterCellType.TYPE_16BIT_U; import static org.geotools.arcsde.raster.info.RasterCellType.TYPE_1BIT; import static org.geotools.arcsde.raster.info.RasterCellType.TYPE_32BIT_REAL; import static org.geotools.arcsde.raster.info.RasterCellType.TYPE_32BIT_S; import static org.geotools.arcsde.raster.info.RasterCellType.TYPE_32BIT_U; import static org.geotools.arcsde.raster.info.RasterCellType.TYPE_4BIT; import static org.geotools.arcsde.raster.info.RasterCellType.TYPE_64BIT_REAL; import static org.geotools.arcsde.raster.info.RasterCellType.TYPE_8BIT_U; import java.awt.Color; import java.awt.Dimension; import java.awt.Transparency; import java.awt.color.ColorSpace; import java.awt.geom.Rectangle2D; import java.awt.image.BandedSampleModel; import java.awt.image.ColorModel; import java.awt.image.ComponentColorModel; import java.awt.image.DataBuffer; import java.awt.image.IndexColorModel; import java.awt.image.SampleModel; import java.util.ArrayList; import java.util.List; import java.util.logging.Logger; import javax.imageio.ImageTypeSpecifier; import org.geotools.coverage.grid.GridEnvelope2D; import org.geotools.coverage.grid.io.OverviewPolicy; import org.geotools.geometry.GeneralEnvelope; import org.geotools.referencing.CRS; import org.geotools.resources.image.ColorUtilities; import org.geotools.resources.image.ComponentColorModelJAI; import org.geotools.util.NumberRange; import org.geotools.util.logging.Logging; import org.opengis.coverage.grid.GridEnvelope; import org.opengis.geometry.Envelope; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.operation.MathTransform; import org.opengis.referencing.operation.MathTransform2D; import org.opengis.referencing.operation.NoninvertibleTransformException; import org.opengis.referencing.operation.TransformException; import com.sun.imageio.plugins.common.BogusColorSpace; /** * * @author Gabriel Roldan (OpenGeo) * @since 2.5.4 * @version $Id$ * * @source $URL$ * http://svn.osgeo.org/geotools/trunk/modules/plugin/arcsde/datastore/src/main/java/org * /geotools/arcsde/raster/info/RasterUtils.java $ */ @SuppressWarnings({ "nls" }) public class RasterUtils { private static final Logger LOGGER = Logging.getLogger("org.geotools.arcsde.gce"); private RasterUtils() { // do nothing } /** * Returns the grid range specifying the matching tiles for a given pyramid level and grid * extent specifying the overlapping area to request in the level's pixel space. * * @param pixelRange * @param tilesHigh * @param tilesWide * @param tileSize * @param numTilesHigh * @param numTilesWide * * @param pixelRange * @param level * * @return a grid range holding the coordinates in tile space that fully covers the requested * pixel range for the given pyramid level, or a negative area range */ private static GridEnvelope findMatchingTiles(final Dimension tileSize, int numTilesWide, int numTilesHigh, final GridEnvelope pixelRange) { final int minPixelX = pixelRange.getLow(0); final int minPixelY = pixelRange.getLow(1); int minTileX = (int) Math.floor(minPixelX / tileSize.getWidth()); int minTileY = (int) Math.floor(minPixelY / tileSize.getHeight()); int numTilesX = (int) Math.ceil(pixelRange.getSpan(0) / tileSize.getWidth()); int numTilesY = (int) Math.ceil(pixelRange.getSpan(1) / tileSize.getHeight()); int maxTiledX = (minTileX + numTilesX) * tileSize.width; int maxTiledY = (minTileY + numTilesY) * tileSize.height; if (maxTiledX < pixelRange.getHigh(0) && (minTileX + numTilesX) < numTilesWide) { numTilesX++; } if (maxTiledY < pixelRange.getHigh(1) && (minTileY + numTilesY) < numTilesHigh) { numTilesY++; } GridEnvelope2D matchingTiles = new GridEnvelope2D(minTileX, minTileY, numTilesX, numTilesY); return matchingTiles; } private static GridEnvelope getTargetGridRange(final MathTransform modelToRaster, final Envelope requestedEnvelope) { GridEnvelope levelOverlappingPixels; int levelMinPixelX; int levelMaxPixelX; int levelMinPixelY; int levelMaxPixelY; { // use a model to raster transform to find out which pixel range at the specified level // better match the requested extent GeneralEnvelope requestedPixels; try { requestedPixels = CRS.transform(modelToRaster, requestedEnvelope); } catch (NoninvertibleTransformException e) { throw new IllegalArgumentException(e); } catch (TransformException e) { throw new IllegalArgumentException(e); } levelMinPixelX = (int) Math.floor(requestedPixels.getMinimum(0)); levelMinPixelY = (int) Math.floor(requestedPixels.getMinimum(1)); levelMaxPixelX = (int) Math.ceil(requestedPixels.getMaximum(0)); levelMaxPixelY = (int) Math.ceil(requestedPixels.getMaximum(1)); final int width = levelMaxPixelX - levelMinPixelX; final int height = levelMaxPixelY - levelMinPixelY; levelOverlappingPixels = new GridEnvelope2D(levelMinPixelX, levelMinPixelY, width, height); } return levelOverlappingPixels; } /** * Creates an IndexColorModel out of a DataBuffer obtained from an ArcSDE's raster color map. * * @param colorMapData * @return */ public static IndexColorModel sdeColorMapToJavaColorModel(final DataBuffer colorMapData, final int bitsPerSample) { if (colorMapData == null) { throw new NullPointerException("colorMapData"); } if (colorMapData.getNumBanks() < 3 || colorMapData.getNumBanks() > 4) { throw new IllegalArgumentException("colorMapData shall have 3 or 4 banks: " + colorMapData.getNumBanks()); } if (bitsPerSample != 8 && bitsPerSample != 16) { throw new IllegalAccessError("bits per sample shall be either 8 or 16. Got " + bitsPerSample); } final int numBanks = colorMapData.getNumBanks(); final int mapSize = colorMapData.getSize(); byte[] r = new byte[mapSize]; byte[] g = new byte[mapSize]; byte[] b = new byte[mapSize]; byte[] a = new byte[mapSize]; for (int i = 0; i < mapSize; i++) { r[i] = (byte) (colorMapData.getElem(0, i) & 0xFF); g[i] = (byte) (colorMapData.getElem(1, i) & 0xFF); b[i] = (byte) (colorMapData.getElem(2, i) & 0xFF); a[i] = (byte) ((numBanks == 3 ? 255 : colorMapData.getElem(3, i)) & 0xFF); } IndexColorModel colorModel = new IndexColorModel(bitsPerSample, mapSize, r, g, b, a); return colorModel; } public static ImageTypeSpecifier createFullImageTypeSpecifier( final RasterDatasetInfo rasterInfo, final int rasterIndex) { final int numberOfBands = rasterInfo.getNumBands(); final RasterCellType nativePixelType = rasterInfo.getNativeCellType(); final RasterCellType pixelType = rasterInfo.getTargetCellType(rasterIndex); // Prepare temporary colorModel and sample model, needed to build the final // ArcSDEPyramidLevel level; final int sampleImageWidth = 1;// rasterInfo.getImageWidth(); final int sampleImageHeight = 1;// rasterInfo.getImageHeight(); final ImageTypeSpecifier its; // treat special cases... final int bitsPerSample = pixelType.getBitsPerSample(); final int dataType = pixelType.getDataBufferType(); final boolean hasColorMap = rasterInfo.isColorMapped(); if (hasColorMap) { // special case, a single band colormapped image IndexColorModel colorMap = rasterInfo.getColorMap(rasterIndex); its = createColorMappedImageSpec(colorMap, sampleImageWidth, sampleImageHeight); } else if (nativePixelType == TYPE_1BIT && numberOfBands == 1) { byte noDataValue = rasterInfo.getNoDataValue(rasterIndex, 0).byteValue(); // special case, a single band 1-bit its = createOneBitColorMappedImageSpec(sampleImageWidth, sampleImageHeight, noDataValue); } else if (nativePixelType == TYPE_4BIT && numberOfBands == 1) { byte noDataValue = rasterInfo.getNoDataValue(rasterIndex, 0).byteValue(); // special case, a single band 4-bit its = createFourBitColorMappedImageSpec(sampleImageWidth, sampleImageHeight, noDataValue); } else if (numberOfBands == 1) { // special case, a single band grayscale image, no matter the pixel depth its = createGrayscaleImageSpec(sampleImageWidth, sampleImageHeight, dataType, bitsPerSample); } else if (numberOfBands == 3 && pixelType == TYPE_8BIT_U) { // special case, an optimizable RGB image its = createRGBImageSpec(sampleImageWidth, sampleImageHeight, dataType); } else if (numberOfBands == 4 && pixelType == TYPE_8BIT_U) { // special case, an optimizable RGBA image its = createRGBAImageSpec(sampleImageWidth, sampleImageHeight, dataType); } else { /* * not an special case, go for a more generic sample model, potentially slower than the * special case ones, but that'll work anyway */ final ColorModel colorModel; final SampleModel sampleModel; { final ColorSpace colorSpace; colorSpace = new BogusColorSpace(numberOfBands); int[] numBits = new int[numberOfBands]; for (int i = 0; i < numberOfBands; i++) { numBits[i] = bitsPerSample; } colorModel = new ComponentColorModelJAI(colorSpace, numBits, false, false, Transparency.OPAQUE, dataType); } { int[] bankIndices = new int[numberOfBands]; int[] bandOffsets = new int[numberOfBands]; // int bandOffset = (tileWidth * tileHeight * pixelType.getBitsPerSample()) / 8; for (int i = 0; i < numberOfBands; i++) { bankIndices[i] = i; bandOffsets[i] = 0;// (i * bandOffset); } sampleModel = new BandedSampleModel(dataType, sampleImageWidth, sampleImageHeight, sampleImageWidth, bankIndices, bandOffsets); } its = new ImageTypeSpecifier(colorModel, sampleModel); } return its; } private static ImageTypeSpecifier createFourBitColorMappedImageSpec(int sampleImageWidth, int sampleImageHeight, byte noDataValue) { int maxValue = (int) TYPE_4BIT.getSampleValueRange().getMaximum(); int mapSize = noDataValue > maxValue ? noDataValue : maxValue + 1; int[] cmap = new int[mapSize]; ColorUtilities.expand(new Color[] { Color.BLACK, Color.WHITE }, cmap, 0, maxValue); for (int i = maxValue; i < mapSize; i++) { cmap[i] = ColorUtilities.getIntFromColor(0, 0, 0, 0); } int transparentPixel = noDataValue; IndexColorModel colorModel = new IndexColorModel(8, mapSize, cmap, 0, true, transparentPixel, DataBuffer.TYPE_BYTE); SampleModel sampleModel = colorModel.createCompatibleSampleModel(sampleImageWidth, sampleImageHeight); ImageTypeSpecifier its = new ImageTypeSpecifier(colorModel, sampleModel); return its; } private static ImageTypeSpecifier createOneBitColorMappedImageSpec(int sampleImageWidth, int sampleImageHeight, byte noDataValue) { assert noDataValue == 2; final int FALSE = ColorUtilities.getIntFromColor(255, 255, 255, 255); final int TRUE = ColorUtilities.getIntFromColor(0, 0, 0, 255); final int NODATA = ColorUtilities.getIntFromColor(255, 255, 255, 0); final int mapSize = 3; int[] cmap = new int[mapSize]; cmap[0] = FALSE; cmap[1] = TRUE; cmap[2] = NODATA; int transparentPixel = noDataValue; IndexColorModel colorModel = new IndexColorModel(8, mapSize, cmap, 0, false, transparentPixel, DataBuffer.TYPE_BYTE); SampleModel sampleModel = colorModel.createCompatibleSampleModel(sampleImageWidth, sampleImageHeight); ImageTypeSpecifier its = new ImageTypeSpecifier(colorModel, sampleModel); return its; } private static ImageTypeSpecifier createRGBAImageSpec(int sampleImageWidth, int sampleImageHeight, final int dataType) { final ImageTypeSpecifier its; ColorSpace colorSpace = ColorSpace.getInstance(ColorSpace.CS_sRGB); boolean hasAlpha = true; boolean isAlphaPremultiplied = false; int transparency = Transparency.TRANSLUCENT; int transferType = dataType; int[] nBits = { 8, 8, 8, 8 }; ColorModel colorModel = new ComponentColorModelJAI(colorSpace, nBits, hasAlpha, isAlphaPremultiplied, transparency, transferType); /* * Do not use colorModel.createCompatibleSampleModel cause it creates a * PixelInterleavedSampleModel and we need a BandedSampleModel so it matches how the data * comes out of ArcSDE */ SampleModel sampleModel = new BandedSampleModel(dataType, sampleImageWidth, sampleImageHeight, 4); its = new ImageTypeSpecifier(colorModel, sampleModel); return its; } private static ImageTypeSpecifier createRGBImageSpec(int sampleImageWidth, int sampleImageHeight, final int dataType) { final ImageTypeSpecifier its; ColorSpace colorSpace = ColorSpace.getInstance(ColorSpace.CS_sRGB); boolean hasAlpha = false; boolean isAlphaPremultiplied = false; int transparency = Transparency.OPAQUE; int transferType = dataType; ColorModel colorModel = new ComponentColorModel(colorSpace, new int[] { 8, 8, 8 }, hasAlpha, isAlphaPremultiplied, transparency, transferType); SampleModel sampleModel = new BandedSampleModel(dataType, sampleImageWidth, sampleImageHeight, 3); its = new ImageTypeSpecifier(colorModel, sampleModel); return its; } private static ImageTypeSpecifier createGrayscaleImageSpec(int sampleImageWidth, int sampleImageHeight, final int dataType, int bitsPerPixel) { final ImageTypeSpecifier its; ColorSpace colorSpace = ColorSpace.getInstance(ColorSpace.CS_GRAY); boolean hasAlpha = false; boolean isAlphaPremultiplied = false; int transparency = Transparency.OPAQUE; int transferType = dataType; int[] nbits = { bitsPerPixel }; ColorModel colorModel = new ComponentColorModelJAI(colorSpace, nbits, hasAlpha, isAlphaPremultiplied, transparency, transferType); SampleModel sampleModel = colorModel.createCompatibleSampleModel(sampleImageWidth, sampleImageHeight); its = new ImageTypeSpecifier(colorModel, sampleModel); return its; } private static ImageTypeSpecifier createColorMappedImageSpec(final IndexColorModel colorModel, int sampleImageWidth, int sampleImageHeight) { final SampleModel sampleModel; final ImageTypeSpecifier its; LOGGER.fine("Found single-band colormapped raster, using its index color model"); sampleModel = colorModel.createCompatibleSampleModel(sampleImageWidth, sampleImageHeight); its = new ImageTypeSpecifier(colorModel, sampleModel); return its; } /** * Given a collection of {@link RasterQueryInfo} instances holding information about how a * request fits for each individual raster composing a catalog, figure out where their resulting * images fit into the overall mosaic that's gonna be the result of the request. * * @param rasterInfo * @param resultEnvelope * @param results * @return */ public static GridEnvelope setMosaicLocations(final RasterDatasetInfo rasterInfo, final List<RasterQueryInfo> results) { final MathTransform modelToRaster; final MathTransform rasterToModel; int minx = Integer.MAX_VALUE; int miny = Integer.MAX_VALUE; int maxx = Integer.MIN_VALUE; int maxy = Integer.MIN_VALUE; { /* * Of all the rasters that match the requested envelope, chose the one with the lowest * resolution as the base to compute the final mosaic layout, so we avoid JAI upsamples, * which are buggy and produce repeated patterns over the x axis instead of just scaling * up the image. */ RasterQueryInfo dimensionChoice = findLowestResolution(results); Long rasterId = dimensionChoice.getRasterId(); int pyramidLevel = dimensionChoice.getPyramidLevel(); int rasterIndex = rasterInfo.getRasterIndex(rasterId); rasterToModel = rasterInfo.getRasterToModel(rasterIndex, pyramidLevel); try { modelToRaster = rasterToModel.inverse(); } catch (NoninvertibleTransformException e) { throw new RuntimeException(e); } } for (RasterQueryInfo rasterResultInfo : results) { final GeneralEnvelope rasterResultEnvelope = rasterResultInfo.getResultEnvelope(); final GridEnvelope targetRasterGridRange; targetRasterGridRange = getTargetGridRange(modelToRaster, rasterResultEnvelope); rasterResultInfo.setMosaicLocation(targetRasterGridRange); minx = Math.min(minx, targetRasterGridRange.getLow(0)); miny = Math.min(miny, targetRasterGridRange.getLow(1)); maxx = Math.max(maxx, targetRasterGridRange.getHigh(0)); maxy = Math.max(maxy, targetRasterGridRange.getHigh(1)); } final GridEnvelope2D mosaicDimension; mosaicDimension = new GridEnvelope2D(minx, miny, 1 + (maxx - minx), 1 + (maxy - miny)); return mosaicDimension; } public static RasterQueryInfo findLowestResolution(List<RasterQueryInfo> results) { double[] prev = { Double.MIN_VALUE, Double.MIN_VALUE }; RasterQueryInfo lowestResQuery = null; double[] curr; for (RasterQueryInfo query : results) { curr = query.getResolution(); if (curr[0] > prev[0]) { prev = curr; lowestResQuery = query; } } return lowestResQuery; } /** * Find out the raster ids and their pyramid levels in the raster dataset for the rasters whose * envelope overlaps the requested one * * @param rasterInfo * @param requestedEnvelope * @param requestedDim * @param overviewPolicy * @return */ public static List<RasterQueryInfo> findMatchingRasters(final RasterDatasetInfo rasterInfo, final GeneralEnvelope requestedEnvelope, final GridEnvelope requestedDim, final OverviewPolicy overviewPolicy) { final int numRasters = rasterInfo.getNumRasters(); List<RasterQueryInfo> matchingRasters = new ArrayList<RasterQueryInfo>(numRasters); int optimalPyramidLevel; GeneralEnvelope gridEnvelope; for (int rasterN = 0; rasterN < numRasters; rasterN++) { optimalPyramidLevel = rasterInfo.getOptimalPyramidLevel(rasterN, overviewPolicy, requestedEnvelope, requestedDim); gridEnvelope = rasterInfo.getGridEnvelope(rasterN, optimalPyramidLevel); final boolean edgesInclusive = true; if (requestedEnvelope.intersects(gridEnvelope, edgesInclusive)) { RasterQueryInfo match = new RasterQueryInfo(); match.setRequestedEnvelope(requestedEnvelope); match.setRequestedDim(requestedDim); match.setRasterId(rasterInfo.getRasterId(rasterN)); match.setRasterIndex(rasterN); match.setPyramidLevel(optimalPyramidLevel); match.setResolution(rasterInfo.getResolution(rasterN, optimalPyramidLevel)); matchingRasters.add(match); } } return matchingRasters; } public static void fitRequestToRaster(final GeneralEnvelope requestedEnvelope, final RasterDatasetInfo rasterInfo, final RasterQueryInfo query) { GridEnvelope resultGridRange; GeneralEnvelope resultEnvelope; // GridEnvelope resultDimensionInsideTiledImage; GridEnvelope tiledImageGridRange; // GridEnvelope levelTileRange; GridEnvelope matchingTiles; int rasterIndex = query.getRasterIndex(); int pyramidLevel = query.getPyramidLevel(); MathTransform2D rasterToModel = (MathTransform2D) rasterInfo.getRasterToModel(rasterIndex, pyramidLevel); MathTransform2D modelToRaster; try { modelToRaster = (MathTransform2D) rasterToModel.inverse(); final GeneralEnvelope adjustedGRange = CRS.transform(modelToRaster, requestedEnvelope); int xmin = (int) Math.floor(adjustedGRange.getMinimum(0)); int ymin = (int) Math.floor(adjustedGRange.getMinimum(1)); int xmax = (int) Math.ceil(adjustedGRange.getMaximum(0)); int ymax = (int) Math.ceil(adjustedGRange.getMaximum(1)); final GridEnvelope levelRange = rasterInfo.getGridRange(rasterIndex, pyramidLevel); xmin = Math.max(xmin, levelRange.getLow(0)); ymin = Math.max(ymin, levelRange.getLow(1)); xmax = Math.min(xmax, levelRange.getHigh(0) + 1);// 1+ because getHigh is inclusive ymax = Math.min(ymax, levelRange.getHigh(1) + 1);// 1+ because getHigh is inclusive int width = xmax - xmin; int height = ymax - ymin; resultGridRange = new GridEnvelope2D(xmin, ymin, width, height); Rectangle2D finalEnvelope = CRS.transform(rasterToModel, (Rectangle2D) resultGridRange, new Rectangle2D.Double()); resultEnvelope = new GeneralEnvelope(finalEnvelope); CoordinateReferenceSystem crs = rasterInfo.getCoverageCrs(); resultEnvelope.setCoordinateReferenceSystem(crs); } catch (TransformException e) { throw new RuntimeException(e); } matchingTiles = findMatchingTiles(rasterInfo, rasterIndex, pyramidLevel, resultGridRange); tiledImageGridRange = getTiledImageGridRange(rasterInfo.getTileDimension(rasterIndex), matchingTiles); query.setResultEnvelope(resultEnvelope); query.setResultGridRange(resultGridRange); query.setMatchingTiles(matchingTiles); query.setTiledImageGridRange(tiledImageGridRange); // query.setResultDimensionInsideTiledImage(resultDimensionInsideTiledImage); // query.setLevelTileRange(levelTileRange); } private static GridEnvelope getTiledImageGridRange(Dimension tileSize, GridEnvelope matchingTiles) { int tiledImageMinX = (matchingTiles.getLow(0) * tileSize.width); int tiledImageMinY = (matchingTiles.getLow(1) * tileSize.height); int tiledWidth = (matchingTiles.getSpan(0) * tileSize.width); int tiledHeight = (matchingTiles.getSpan(1) * tileSize.height); GridEnvelope2D tiledImageGridRange; tiledImageGridRange = new GridEnvelope2D(tiledImageMinX, tiledImageMinY, tiledWidth, tiledHeight); return tiledImageGridRange; } private static GridEnvelope findMatchingTiles(RasterDatasetInfo rasterInfo, int rasterIndex, int pyramidLevel, GridEnvelope resultGridRange) { final Dimension tileSize = rasterInfo.getTileDimension(rasterIndex); final int numTilesWide = rasterInfo.getNumTilesWide(rasterIndex, pyramidLevel); final int numTilesHigh = rasterInfo.getNumTilesHigh(rasterIndex, pyramidLevel); GridEnvelope matchingTiles; matchingTiles = findMatchingTiles(tileSize, numTilesWide, numTilesHigh, resultGridRange); return matchingTiles; } /** * Returns a color model based on {@code colorMap} that's guaranteed to have at least one * transparent pixel whose index can be used as no-data value for colormapped rasters, even if * the returned IndexColorModel needs to be of a higher sample depth (ie, 16 instead of 8 bit) * to satisfy that. * * @param colorMap * the raster's native color map the returned one will be based on * @return the same {@code colorMap} if it has a transparent pixel, another, possibly of a * higher depth one if not, containing all the colors from {@code colorMap} and a newly * allocated cell for the transparent pixel if necessary */ public static IndexColorModel ensureNoDataPixelIsAvailable(final IndexColorModel colorMap) { int transparentPixel = colorMap.getTransparentPixel(); if (transparentPixel > -1) { return colorMap; } final int transferType = colorMap.getTransferType(); final int mapSize = colorMap.getMapSize(); final int maxSize = 65536;// true for either transfer type if (mapSize == maxSize) { LOGGER.fine("There's no room for a new transparent pixel, " + "returning the original colorMap as is"); return colorMap; } /* * The original map size is lower than the maximum allowed by a UShort color map, so expand * the colormap by one and make that new entry transparent */ final int newMapSize = mapSize + 1; final int[] argb = new int[newMapSize]; colorMap.getRGBs(argb); // set the last entry as transparent argb[newMapSize - 1] = ColorUtilities.getIntFromColor(0, 0, 0, 0); IndexColorModel targetColorModel; final int significantBits; final int newTransferType; { if (DataBuffer.TYPE_BYTE == transferType && newMapSize <= 256) { /* * REVISIT: check if this needs to be promoted depending on whether I decide to * treat 1 and 4 bit images as indexed with 1 and 4 significant bits respectively */ significantBits = colorMap.getPixelSize(); newTransferType = DataBuffer.TYPE_BYTE; } else if (DataBuffer.TYPE_BYTE == transferType && newMapSize == 257) { // it's being promoted. significantBits = 9 makes for a 512 color model instead of a // 65535 one saving a good bit of memory, specially for color mapped raster catalogs // where the colormodel for each raster in the catalog is to be held in memory significantBits = 9; newTransferType = DataBuffer.TYPE_USHORT; } else { // already was 16-bit significantBits = 16; newTransferType = DataBuffer.TYPE_USHORT; } } final int transparentPixelIndex = newMapSize - 1; final boolean hasalpha = true; final int startIndex = 0; targetColorModel = new IndexColorModel(significantBits, newMapSize, argb, startIndex, hasalpha, transparentPixelIndex, newTransferType); return targetColorModel; } /** * For a color-mapped raster, the no-data value is set to the * {@link IndexColorModel#getTransparentPixel() transparent pixel} * * @param colorMap * @return the index in the colorMap that's the transparent pixel as is to be used as no-data * value */ public static Number determineNoDataValue(IndexColorModel colorMap) { int noDataPixel = colorMap.getTransparentPixel(); if (-1 == noDataPixel) { // there were no room for a transparent pixel, find out the closest match noDataPixel = ColorUtilities.getTransparentPixel(colorMap); } return Integer.valueOf(noDataPixel); } /** * @param numBands * number of bands in the raster dataset for the band whose nodata value is to be * determined. Might be useful to treat special cases where some assumptions are made * depending on the cell type and number of bands * @param statsMin * the minimum sample value for the band as reported by the band's statistics, or * {@code NaN} * @param statsMax * the maximum sample value for the band as reported by the band's statistics, or * {@code NaN} * @param nativeCellType * the band's native cell type * @return */ public static Number determineNoDataValue(final int numBands, final double statsMin, final double statsMax, final RasterCellType nativeCellType) { final Number nodata; if (nativeCellType == TYPE_32BIT_REAL) { LOGGER.fine("no data value is Float.NaN"); return Float.valueOf(Float.NaN); } else if (nativeCellType == TYPE_64BIT_REAL) { LOGGER.fine("no data value is Double.NaN"); return Double.valueOf(Double.NaN); } else if (nativeCellType == TYPE_1BIT) { LOGGER.fine("1BIT images no-data value is set to 2," + " regardless of the raster statistics"); return Double.valueOf(2); } else if (nativeCellType == TYPE_4BIT) { LOGGER.fine("4BIT images no-data value is set to 16," + " regardless of the raster statistics"); return Double.valueOf(16); } else if (!isGeoPhysics(numBands, nativeCellType)) { LOGGER.fine("3 or 4 band, 8 bit unsigned image, assumed to be " + "RGB or RGBA respectively and nodata value hardcoded to 255"); return (Number) nativeCellType.getSampleValueRange().getMaxValue(); } final NumberRange<?> sampleValueRange = nativeCellType.getSampleValueRange(); final double minimumSample = sampleValueRange.getMinimum(true); final double maximumSample = sampleValueRange.getMaximum(true); double lower; double greater; if (Double.isNaN(statsMin) || Double.isNaN(statsMax)) { lower = Math.ceil(minimumSample - 1); greater = Math.floor(maximumSample + 1); } else { lower = Math.ceil(statsMin - 1); greater = Math.floor(statsMax + 1); } final boolean isUnsigned = minimumSample == 0; if (sampleValueRange.contains((Number) Double.valueOf(lower))) { // lower is ok nodata = lower; } else if (sampleValueRange.contains((Number) Double.valueOf(greater))) { // upper is ok nodata = greater; } else if (isUnsigned) { // need to set no-data to the higher value, floor is zero nodata = greater; // if (cellType == TYPE_1BIT || cellType == TYPE_4BIT) { // nodata = greater; // } else { // // best guess without promoting. We don't actually want to promote a raster that is // // non // // colormapped and either has no statistics or it's range is full to preserve the // // cases // // were it may affect badly the visualization (for example, a 3 band 8bit raster // // promoted to 3 band 16bit is gonna look almost black // nodata = maximumSample; // } } else { // no-data as the lower value is ok, floor is non zero (the celltype is signed) nodata = lower; } return nodata; } public static boolean isGeoPhysics(final int numBands, final RasterCellType nativeCellType) { boolean geophysics = true; if (nativeCellType == TYPE_8BIT_U && (numBands == 3 || numBands == 4)) { geophysics = false; } return geophysics; } public static RasterCellType determineTargetCellType(final RasterCellType nativeCellType, final List<Number> noDataValues) { if (TYPE_32BIT_REAL == nativeCellType || TYPE_64BIT_REAL == nativeCellType) { // no data value is NaN, so no need to promote. For other types NaN is not available for (Number nodata : noDataValues) { if (!Double.isNaN(nodata.doubleValue())) { throw new IllegalArgumentException("no data values for float and " + "double cell types shall be NaN: " + nodata); } } return nativeCellType; } // find a cell type that's deep enough for all the bands in the given raster double noDataMin = Double.POSITIVE_INFINITY, noDataMax = Double.NEGATIVE_INFINITY; { for (Number noData : noDataValues) { noDataMin = Math.min(noDataMin, noData.doubleValue()); noDataMax = Math.max(noDataMax, noData.doubleValue()); } } final NumberRange<Double> sampleValueRange; sampleValueRange = nativeCellType.getSampleValueRange().castTo(Double.class); final RasterCellType targetCellType; if (sampleValueRange.contains((Number) Double.valueOf(noDataMin)) && sampleValueRange.contains((Number) Double.valueOf(noDataMax))) { /* * The native cell type can hold the no-data values for all bands in the raster */ targetCellType = nativeCellType; } else { targetCellType = promote(nativeCellType); } return targetCellType; } private static RasterCellType promote(final RasterCellType nativeCellType) { switch (nativeCellType) { case TYPE_1BIT: case TYPE_4BIT: return TYPE_8BIT_U; case TYPE_8BIT_U: return TYPE_16BIT_U; case TYPE_8BIT_S: return TYPE_16BIT_S; case TYPE_16BIT_U: return TYPE_32BIT_U; case TYPE_16BIT_S: return TYPE_32BIT_S; case TYPE_32BIT_S: case TYPE_32BIT_REAL: case TYPE_32BIT_U: return TYPE_64BIT_REAL; default: throw new IllegalArgumentException( "Can't promote a raster of type 64-bit-real, there's " + "no higher pixel depth than that!"); } } }