/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2001-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.coverage.grid; import java.awt.RenderingHints; import java.awt.color.ColorSpace; import java.awt.image.*; // Numerous imports here. import java.awt.image.renderable.ParameterBlock; import java.util.Arrays; import java.util.Collection; import java.util.EnumMap; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.logging.Logger; import java.util.logging.LogRecord; import javax.media.jai.ImageLayout; import javax.media.jai.JAI; import javax.media.jai.LookupTableJAI; import javax.media.jai.NullOpImage; import javax.media.jai.PlanarImage; import javax.media.jai.ROI; import javax.media.jai.RenderedOp; import javax.media.jai.operator.FormatDescriptor; import javax.media.jai.operator.LookupDescriptor; import static java.lang.Double.isNaN; import org.opengis.util.InternationalString; import org.opengis.coverage.grid.GridCoverage; import org.opengis.referencing.operation.MathTransform1D; import org.opengis.referencing.operation.TransformException; import org.opengis.referencing.operation.NoninvertibleTransformException; import org.geotools.coverage.Category; import org.geotools.coverage.GridSampleDimension; import org.geotools.coverage.processing.CoverageProcessor; import org.geotools.factory.Hints; import org.geotools.resources.XArray; import org.geotools.resources.coverage.CoverageUtilities; import org.geotools.resources.i18n.Errors; import org.geotools.resources.i18n.ErrorKeys; import org.geotools.resources.i18n.Loggings; import org.geotools.resources.i18n.LoggingKeys; import org.geotools.resources.i18n.Vocabulary; import org.geotools.resources.i18n.VocabularyKeys; import org.geotools.resources.image.ColorUtilities; import org.geotools.resources.image.ImageUtilities; import org.geotools.util.Utilities; import org.geotools.util.NumberRange; import org.geotools.util.logging.Logging; /** * Holds the different views of a {@link GridCoverage2D}. Those views are handled in a separated * class because the same instance may be shared by more than one {@link GridCoverage2D}. Because * views are associated with potentially big images, sharing them when possible is a big memory * and CPU saver. * * @source $URL$ * @version $Id$ * @author Martin Desruisseaux (IRD) */ final class ViewsManager { /** * Slight number for rounding errors in floating point comparaison. */ private static final float EPS = 1E-5f; /** * The views. The coverage that created this {@code ViewsManager} must be stored under * the {@link ViewType#NATIVE} key. */ private final Map views; /** * Constructs a map of views. * * @param coverage The coverage that created this {@code ViewsManager}. */ private ViewsManager(final GridCoverage2D coverage) { views = new EnumMap(ViewType.class); boolean geophysics = true; // 'true' only if all bands are geophysics. boolean photographic = true; // 'true' only if no band have category. final int numBands = coverage.getNumSampleDimensions(); scan: for (int i=0; i categories = band.getCategories(); if (categories == null || categories.isEmpty()) { // No category. The image is treated as photographic. continue; } photographic = false; final GridSampleDimension packed = band.geophysics(false); if (band != packed) { for (final Category category : packed.getCategories()) { if (category.isQuantitative()) { // Preserves the geophysics value if at least one category // is quantitative. Otherwise it will be set to 'false'. continue scan; } } } geophysics = false; } } final ViewType type; if (photographic) { // Must be tested first because 'geophysics' it 'true' as well in this case. type = ViewType.PHOTOGRAPHIC; } else if (geophysics) { type = ViewType.GEOPHYSICS; } else { type = ViewType.PACKED; } views.put(type, coverage); views.put(ViewType.NATIVE, coverage.getNativeView()); } /** * Returns a shared map of views or constructs a new one. In order to check if we can * share the views with one of the sources, the source must have identical image, geometry and * sample dimensions. As a safety, we do not allow views sharing for arbitrary classes of * {@link Calculator2D} (this is checked by {@code viewClass}). * * @param coverage The coverage that wants to create a {@code ViewsManager}. */ static ViewsManager create(final GridCoverage2D coverage) { final Class viewClass = coverage.getViewClass(); if (viewClass != null) { Collection sources = coverage.getSources(); while (sources != null) { Collection next = null; for (final GridCoverage source : sources) { if (source instanceof GridCoverage2D) { final GridCoverage2D candidate = (GridCoverage2D) source; if (Utilities.equals(coverage.image, candidate.image) && Utilities.equals(coverage.gridGeometry, candidate.gridGeometry) && Arrays .equals(coverage.sampleDimensions, candidate.sampleDimensions) && viewClass.equals(candidate.getViewClass())) // The CRS is checked with the GridGeometry2D. { return candidate.copyViewsTo(coverage); } } if (source == null) { continue; } final Collection more = source.getSources(); if (more != null && !more.isEmpty()) { if (next == null) { next = new LinkedHashSet(more); } else { next.addAll(more); } } } sources = next; } } return new ViewsManager(coverage); } /** * Invoked by {@linkplain GridCoverage2D#view} for getting a view. * * NOTE: {@link GridCoverage2D#toString()} requires that this method is * synchronized on {@code this}. */ public synchronized GridCoverage2D get(final GridCoverage2D caller, final ViewType type, final Hints userHints) { GridCoverage2D coverage = views.get(type); if (coverage != null) { return coverage; } coverage = views.get(ViewType.NATIVE); if (coverage == null) { // TODO: localize. throw new IllegalStateException("This coverage has been disposed."); } switch (type) { case RENDERED: coverage = rendered(caller, userHints); break; case PACKED: coverage = geophysics(coverage, false, userHints); break; case GEOPHYSICS: coverage = geophysics(coverage, true, userHints); break; case PHOTOGRAPHIC: coverage = photographic(coverage, userHints); break; default: { /* * We don't want a case for: * - SAME because it should be handled by "GridCoverage2D.view(...)" * - NATIVE because it should be handled by "views.get(type)". * * Getting there with one of the above type is an error. */ throw new IllegalArgumentException(Errors.format( ErrorKeys.ILLEGAL_ARGUMENT_$2, "type", type)); } } coverage = caller.specialize(coverage); if (caller.copyViewsTo(coverage) != this) { throw new AssertionError(); // Should never happen. } views.put(type, coverage); return coverage; } /** * Invoked by {@link #create} when a photographic view needs to be created. This method * reformats the {@linkplain ColorModel color model} to a {@linkplain ComponentColorModel * component color model} preserving transparency. The new color model is typically backed * by an RGB {@linkplain ColorSpace color space}, but not necessarly. It could be YMCB as well. */ @SuppressWarnings("fallthrough") private static GridCoverage2D photographic(final GridCoverage2D coverage, final Hints userHints) { final RenderedImage image = coverage.getRenderedImage(); final ColorModel cm = image.getColorModel(); /* * If the image already use a component color model (not necessarly backed by * RGB color space - it could be CYMB as well), then there is nothing to do. */ if (cm instanceof ComponentColorModel) { return coverage; } final int dataType; final ColorSpace cs; final LookupTableJAI lookup; /* * If the color model is indexed. Converts to RGB or gray scale using a single "Lookup" * operation. Color space will be RGB or GRAY, and type will be DataBuffer.TYPE_BYTE. */ if (cm instanceof IndexColorModel) { final IndexColorModel icm = (IndexColorModel) cm; final int mapSize = icm.getMapSize(); final byte data[][]; if (ColorUtilities.isGrayPalette(icm, false)) { final byte[] gray = new byte[mapSize]; icm.getGreens(gray); if (icm.hasAlpha()) { final byte[] alpha = new byte[mapSize]; icm.getAlphas(alpha); data = new byte[][] { gray, alpha }; } else { data = new byte[][] { gray }; } cs = ColorSpace.getInstance(ColorSpace.CS_GRAY); } else { data = new byte[cm.getNumComponents()][mapSize]; switch (data.length) { default: // Should not occurs, but keep as a paranoiac check. case 4: icm.getAlphas(data[3]); case 3: icm.getBlues (data[2]); case 2: icm.getGreens(data[1]); case 1: icm.getReds (data[0]); case 0: break; } cs = icm.getColorSpace(); } dataType = DataBuffer.TYPE_BYTE; lookup = new LookupTableJAI(data); } else { lookup = null; cs = cm.getColorSpace(); dataType = (cm instanceof DirectColorModel) ? DataBuffer.TYPE_BYTE : image.getSampleModel().getTransferType(); } /* * Gets the rendering hints to be given to the image operation. */ final ColorModel targetCM; final SampleModel targetSM; targetCM = new ComponentColorModel(cs, cm.hasAlpha(), // If true, supports transparency. cm.isAlphaPremultiplied(), // If true, alpha is premultiplied. cm.getTransparency(), // What alpha values can be represented. dataType); // Type of primitive array used to represent pixel. targetSM = targetCM.createCompatibleSampleModel(image.getWidth(), image.getHeight()); RenderingHints hints = ImageUtilities.getRenderingHints(image); if (hints == null) { hints = new RenderingHints(null); } ImageLayout layout = (ImageLayout) hints.get(JAI.KEY_IMAGE_LAYOUT); if (layout == null) { layout = new ImageLayout(); } layout.setColorModel (targetCM); layout.setSampleModel(targetSM); hints.put(JAI.KEY_IMAGE_LAYOUT, layout); /* * Creates the image, than the coverage. */ final RenderedOp view; if (lookup != null) { view = LookupDescriptor.create(image, lookup, hints); } else { view = FormatDescriptor.create(image, dataType, hints); } assert view.getColorModel() instanceof ComponentColorModel; return createView(coverage, view, null, 2, userHints); } /** * Invoked by {@link #create} when a geophysics or packed view needs to be created. * * @todo IndexColorModel seems to badly choose its sample model. As of JDK 1.4-rc1, it * construct a ComponentSampleModel, which is drawn very slowly to the screen. A * much faster sample model is PixelInterleavedSampleModel, which is the sample * model used by BufferedImage for TYPE_BYTE_INDEXED. We should check if this is * fixed in future J2SE release. * * @todo The "Piecewise" operation is disabled because javac 1.4.1_01 generate illegal * bytecode. This bug is fixed in javac 1.4.2-beta. However, we still have an * ArrayIndexOutOfBoundsException in JAI code... */ private static GridCoverage2D geophysics(final GridCoverage2D coverage, final boolean toGeo, final Hints userHints) { /* * STEP 1 - Gets the source image and prepare the target bands (sample dimensions). * As a slight optimisation, we skip the "Null" operation since such image * may be the result of some operation (e.g. "Colormap"). */ RenderedImage image = coverage.image; while (image instanceof NullOpImage) { final NullOpImage op = (NullOpImage) image; if (op.getNumSources() != 1) { break; } image = op.getSourceImage(0); } final SampleModel sourceModel = image.getSampleModel(); final int numBands = sourceModel.getNumBands(); final GridSampleDimension[] sourceBands = coverage.sampleDimensions; final GridSampleDimension[] targetBands = sourceBands.clone(); assert targetBands.length == numBands : targetBands.length; for (int i=0; i sources = sourceBands[i].getCategories(); final int numCategories = sources.size(); float[] sourceBreakpoints = null; float[] targetBreakpoints = null; double expectedSource = Double.NaN; double expectedTarget = Double.NaN; int jbp = 0; // Break point index (vary with j) for (int j=0; j sourceMin) : expectedSource; } sourceBreakpoints[jbp ] = sourceMin; sourceBreakpoints[jbp+1] = sourceMax; targetBreakpoints[jbp ] = targetMin; targetBreakpoints[jbp+1] = targetMax; jbp += 2; expectedSource = range.getMaximum(false); expectedTarget = expectedSource * scale + offset; } breakpoints[i][0] = sourceBreakpoints = XArray.resize(sourceBreakpoints, jbp); breakpoints[i][1] = targetBreakpoints = XArray.resize(targetBreakpoints, jbp); assert XArray.isSorted(sourceBreakpoints); } if (canRescale && scales!=null && (!conditional || isZeroExcluded(image, scales, offsets))) { operation = "Rescale"; param = param.add(scales).add(offsets); } else if (canPiecewise && breakpoints!=null) { // operation = "Piecewise"; // param = param.add(breakpoints); } } catch (TransformException exception) { /* * At least one category doesn't use a linear relation. Ignores the exception and * fallback on the next case. We log a message at Level.FINE rather than WARNING * because this exception may be normal. We pretend that the log come from * GridCoverage2D.view, which is the public method that invoked this one. */ Logging.recoverableException(GridCoverage2D.class, "view", exception); } /* * STEP 5 - Transcode the image sample values. The "SampleTranscode" operation is * registered in the org.geotools.coverage package in the GridSampleDimension * class. */ if (operation == null) { param = param.add(sourceBands); operation = "org.geotools.SampleTranscode"; } final RenderedOp view = JAI.create(operation, param, hints); return createView(coverage, view, targetBands, toGeo ? 1 : 0, userHints); } /** * Invoked by {@link #create} when a rendered view needs to be created. * * @todo Not yet implemented. For now we use the packed view as a close match. Future version * will needs to make sure that we returns the same instance than PACKED when suitable. */ private GridCoverage2D rendered(final GridCoverage2D coverage, final Hints userHints) { return get(coverage, ViewType.PACKED, userHints); } /** * Creates the view and logs a record. */ private static GridCoverage2D createView(final GridCoverage2D coverage, final RenderedOp view, final GridSampleDimension[] targetBands, final int code, final Hints userHints) { final InternationalString name = coverage.getName(); if (GridCoverage2D.LOGGER.isLoggable(CoverageProcessor.OPERATION)) { // Logs a message using the same level than grid coverage processor. final String operation = view.getOperationName(); final String shortName = operation.substring(operation.lastIndexOf('.') + 1); final Locale locale = coverage.getLocale(); final LogRecord record = Loggings.getResources(locale).getLogRecord( CoverageProcessor.OPERATION, LoggingKeys.SAMPLE_TRANSCODE_$3, new Object[] { (name != null) ? name.toString(locale) : Vocabulary.getResources(locale).getString(VocabularyKeys.UNTITLED), Integer.valueOf(code), shortName }); record.setSourceClassName(GridCoverage2D.class.getName()); record.setSourceMethodName("geophysics"); final Logger logger = GridCoverage2D.LOGGER; record.setLoggerName(logger.getName()); logger.log(record); } final GridCoverage[] sources = new GridCoverage[] {coverage}; return new GridCoverage2D(name, view, coverage.gridGeometry, targetBands, sources, null, userHints); } /** * Returns {@code true} if rescaling every pixels in the specified image (excluding NaN) would * not produce zero value. In case of doubt, this method conservatively returns {@code false}. *

* Why this method exists
* When a {@link SampleDimension} describes exactly one linear relationship with one NaN value * mapping exactly to the index value 0, then the "geophysics to native" transform * can be optimized to the {@code "Rescale"} operation because {@link Float#NaN} casted to the * {@code int} primitive type equals 0. This case is very common, which make this optimization * a usefull one. Unfortunatly there is nothing in {@code "Rescale"} preventing some real number * (not NaN) to maps to 0 through the normal linear relationship. We need to make sure that the * range of transformed values doesn't contains 0. */ private static boolean isZeroExcluded(final RenderedImage image, final double[] scales, final double[] offsets) { /* * We can't do any garantee if pixel values are modifiable. */ if (image instanceof WritableRenderedImage) { return false; } /* * If an "Extrema" operation is used somewhere in the image chain, ensure that it was * applied on an image with the same pixel values than the image we want to analyze. * Ensure also that no ROI was defined for the "Extrema" operation. */ Object parent = image; while (parent instanceof PlanarImage) { final PlanarImage planar = (PlanarImage) image; if (parent instanceof RenderedOp) { final RenderedOp op = (RenderedOp) parent; final String name = op.getOperationName(); if (name.equalsIgnoreCase("Extrema")) { final int n = op.getNumParameters(); for (int i=0; i= 2) return false; if (n == 0) break; parent = planar.getSourceObject(0); } /* * Apparently, there is nothing preventing us to query the "extrema" property. Note that * the above test did not garantee that this property is defined - only that if defined, * it looks like suitable. Now ensure that the range after conversion does not includes 0. */ final Object property = image.getProperty("extrema"); if (!(property instanceof double[][])) { return false; } final double[][] extrema = (double[][]) property; if (extrema.length != 2) { return false; } for (int i=0; i maximum) { final double tmp = minimum; minimum = maximum; maximum = tmp; } if (!(minimum > 0 || maximum < 0)) { // Use '!' for catching NaN. return false; } } return true; } /** * Disposes all views and returns the remaining ones. Disposed views are removed from * this {@code ViewsManager}, but may be recreated if the user asks them again. *

* This method is invoked by {@link GridCoverage2D#dispose} method only. */ public synchronized Collection dispose(final boolean force) { /* * Following loop will be executed as long as we have been able to dispose at least one * view. This is because some coverages can be disposed only after their dependency have * been disposed first. Since we don't know the dependency order, we just try the loop * again and again. The amount of values (5) is small enough to keep the cost small. */ int disposed; do { disposed = 0; for (final Iterator it=views.values().iterator(); it.hasNext();) { final GridCoverage2D coverage = it.next(); if (coverage.disposeImage(force)) { it.remove(); disposed++; } } } while (disposed != 0); return views.values(); } }