/* * Geotoolkit.org - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2009-2012, Open Source Geospatial Foundation (OSGeo) * (C) 2009-2012, 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.internal.image.io; import java.util.Locale; import java.io.IOException; import java.util.ArrayList; import java.util.List; import javax.imageio.ImageReader; import javax.imageio.metadata.IIOMetadata; import javax.media.jai.iterator.RectIter; import javax.media.jai.iterator.RectIterFactory; import javax.measure.Unit; import org.opengis.util.InternationalString; import org.opengis.coverage.SampleDimension; import org.opengis.metadata.content.TransferFunctionType; import org.apache.sis.util.ArraysExt; import org.apache.sis.measure.NumberRange; import org.geotoolkit.coverage.GridSampleDimension; import org.geotoolkit.image.internal.ImageUtilities; import org.geotoolkit.image.io.metadata.MetadataNodeAccessor; import static org.geotoolkit.image.io.metadata.SpatialMetadataFormat.GEOTK_FORMAT_NAME; /** * A convenience specialization of {@link MetadataNodeAccessor} for the * {@code "ImageDescription/Dimensions"} node. Example: * * {@preformat java * SpatialMetadata metadata = new SpatialMetadata(SpatialMetadataFormat.getImageInstance(null)); * DimensionAccessor accessor = new DimensionAccessor(metadata); * accessor.selectChild(accessor.appendChild()); * accessor.setValueRange(-100, 2000); * } * * @author Martin Desruisseaux (Geomatys) * @version 3.21 * * @since 3.06 * @module */ public final class DimensionAccessor extends MetadataNodeAccessor { /** * Small tolerance threshold for rounding errors. */ private static final double EPS = 1E-10; /** * Creates a new accessor for the given metadata. * * @param metadata The Image I/O metadata. An instance of the * {@link org.geotoolkit.image.io.metadata.SpatialMetadata} * sub-class is recommended, but not mandatory. */ public DimensionAccessor(final IIOMetadata metadata) { super(metadata, GEOTK_FORMAT_NAME, "ImageDescription/Dimensions", "Dimension"); } /** * Sets the description, transfer function, minimum, maximum and fill values from the * given sample dimension. This convenience method fetches the information from the * given band and delegates to the other setter methods defined in this class. * * @param band The band from which to get the attribute values. * @param locale The locale to use for localizing the description. * * @since 3.17 */ public void setDimension(final SampleDimension band, final Locale locale) { if (band instanceof GridSampleDimension) { setUserObject(band); } final InternationalString description = band.getDescription(); if (description != null) { setDescriptor(description.toString(locale)); } final double minimum = band.getMinimumValue(); final double maximum = band.getMaximumValue(); setValueRange(minimum, maximum); double[] fillValues = band.getNoDataValues(); if (fillValues == null && band instanceof GridSampleDimension) { /* * This may happen if the sample dimension is geophysics. We will accept the fill * values from the non-geophysics view if they are outside the range of geophysics * sample values, so there is no possible confusion. This is needed for example in * NetCDF files, where "fillValues" attribute exists even for geophysics data. */ fillValues = ((GridSampleDimension) band).geophysics(false).getNoDataValues(); if (fillValues != null) { int n = 0; for (int i=0; i<fillValues.length; i++) { final double fillValue = fillValues[i]; if (fillValue < minimum || fillValue > maximum) { fillValues[n++] = fillValue; } } fillValues = ArraysExt.resize(fillValues, n); } } setFillSampleValues(fillValues); setTransfertFunction(band.getScale(), band.getOffset(), null); // TODO: declare transfer function. setUnits(band.getUnits()); } /** * Sets the {@code "descriptor"} attribute to the given value. * * @param descriptor The descriptor, or {@code null} if none. */ public void setDescriptor(final String descriptor) { setAttribute("descriptor", descriptor); } /** * Sets the {@code "units"} attribute to the given value. * * @param units The units, or {@code null} if none. */ public void setUnits(final String units) { setAttribute("units", units); } /** * Sets the {@code "units"} attribute to the given value. * * @param units The units, or {@code null} if none. */ public void setUnits(final Unit<?> units) { setAttribute("units", units); } /** * Sets the {@code "minValue"} and {@code "maxValue"} attributes to the given range. * They are the geophysical value, already transformed by the transfer function if * there is one. * <p> * This method replaces {@link Float#MAX_VALUE} by infinite values, because the * maximum value is often used in many format for meaning "infinity". * * @param minimum The value to be assigned to the {@code "minValue"} attribute. * @param maximum The value to be assigned to the {@code "maxValue"} attribute. */ public void setValueRange(float minimum, float maximum) { if (minimum == -Float.MAX_VALUE) minimum = Float.NEGATIVE_INFINITY; if (maximum == Float.MAX_VALUE) maximum = Float.POSITIVE_INFINITY; setAttribute("minValue", minimum); setAttribute("maxValue", maximum); } /** * Sets the {@code "minValue"} and {@code "maxValue"} attributes to the given range. * They are the geophysical value, already transformed by the transfer function if * there is one. * <p> * This method replaces {@link Double#MAX_VALUE} by infinite values, because the * maximum value is often used in many format for meaning "infinity". * * @param minimum The value to be assigned to the {@code "minValue"} attribute. * @param maximum The value to be assigned to the {@code "maxValue"} attribute. */ public void setValueRange(double minimum, double maximum) { if (minimum == -Double.MAX_VALUE) minimum = Double.NEGATIVE_INFINITY; if (maximum == Double.MAX_VALUE) maximum = Double.POSITIVE_INFINITY; setAttribute("minValue", minimum); setAttribute("maxValue", maximum); } /** * Sets the {@code "validSampleValues"} attribute to the given range. This is the range of * values encoded in the file, before the transformation by the transfer function if there * is one. * <p> * This method does nothing if the given range is infinite. * * @param minimum The minimal sample value, inclusive. * @param maximum The maximal sample value, inclusive. */ public void setValidSampleValue(final double minimum, final double maximum) { if (minimum <= maximum && !Double.isInfinite(minimum) && !Double.isInfinite(maximum)) { setValidSampleValue(NumberRange.createBestFit(minimum, true, maximum, true)); } } /** * Sets the {@code "validSampleValues"} attribute to the given range. This is the range of * values encoded in the file, before the transformation by the transfer function if there * is one. * * @param range The value to be assigned to the {@code "validSampleValues"} attribute. */ public void setValidSampleValue(final NumberRange<?> range) { setAttribute("validSampleValues", range); } /** * Sets the {@code "fillSampleValues"} attribute to the given value. * * @param value The value to be assigned to the {@code "fillSampleValues"} attribute. */ public void setFillSampleValues(final int value) { setAttribute("fillSampleValues", value); } /** * Sets the {@code "fillSampleValues"} attribute to the given array. * * @param values The values to be assigned to the {@code "fillSampleValues"} attribute. */ public void setFillSampleValues(final int... values) { setAttribute("fillSampleValues", values); } /** * Sets the {@code "fillSampleValues"} attribute to the given value. * * @param value The value to be assigned to the {@code "fillSampleValues"} attribute. */ public void setFillSampleValues(final float value) { setAttribute("fillSampleValues", value); } /** * Sets the {@code "fillSampleValues"} attribute to the given array. * * @param values The values to be assigned to the {@code "fillSampleValues"} attribute. */ public void setFillSampleValues(final float... values) { setAttribute("fillSampleValues", values); } /** * Sets the {@code "fillSampleValues"} attribute to the given value. * * @param value The value to be assigned to the {@code "fillSampleValues"} attribute. */ public void setFillSampleValues(final double value) { setAttribute("fillSampleValues", value); } /** * Sets the {@code "fillSampleValues"} attribute to the given array. * * @param values The values to be assigned to the {@code "fillSampleValues"} attribute. */ public void setFillSampleValues(final double... values) { setAttribute("fillSampleValues", values); } /** * Sets the {@code "scaleFactor"}, {@code "offset"} and {@code "transferFunctionType"} * attributes to the given values. * * @param scale The value to be assigned to the {@code "scaleFactor"} attribute. * @param offset The value to be assigned to the {@code "offset"} attribute. * @param type The value to be assigned to the {@code "transferFunctionType"} attribute. */ public void setTransfertFunction(final double scale, final double offset, final TransferFunctionType type) { setAttribute("scaleFactor", scale); setAttribute("offset", offset); setAttribute("transferFunctionType", type); } /** * Sets the minimum and maximum values from the pixel values. This method is costly * and should be invoked only for relatively small images, after we checked that the * extremum are not already declared in the metadata. * * @param reader The image reader to use for reading the pixel values. * @param imageIndex The index of the image to read (usually 0). * @throws IOException If an error occurred while reading the image. * * @since 3.14 */ public void scanValidSampleValue(final ImageReader reader, final int imageIndex) throws IOException { int bandIndex = 0; final RectIter iter = RectIterFactory.create(reader.readAsRenderedImage(imageIndex, null), null); iter.startBands(); if (!iter.finishedBands()) do { if (bandIndex >= childCount()) { bandIndex = appendChild(); } selectChild(bandIndex); setAttribute("minValue", Double.NaN); setAttribute("maxValue", Double.NaN); final double[] padValues = getAttributeAsDoubles("fillSampleValues", true); double min = Double.POSITIVE_INFINITY; double max = Double.NEGATIVE_INFINITY; iter.startLines(); if (!iter.finishedLines()) do { iter.startPixels(); if (!iter.finishedPixels()) { nextPixel: do { final double sample = iter.getSampleDouble(); if (padValues != null) { for (final double v : padValues) { if (sample == v) { continue nextPixel; } } } if (sample < min) min = sample; if (sample > max) max = sample; } while (!iter.nextPixelDone()); } } while (!iter.nextLineDone()); setValidSampleValue(min, max); // Do not invoke setValueRange(min, max) because the // later is about geophysics values, not sample values. bandIndex++; } while (!iter.nextBandDone()); } /** * Returns {@code true} if a call to {@link #scanValidSampleValue(ImageReader, int)} is * recommended. This method uses heuristic rules that may be changed in any future version. * * @param reader The image reader to use for reading information. * @param imageIndex The index of the image to query (usually 0). * @return {@code true} if a call to {@code scanValidSampleValue} is recommended. * @throws IOException If an error occurred while querying the image. * * @since 3.14 */ public boolean isScanSuggested(final ImageReader reader, final int imageIndex) throws IOException { final int numChilds = childCount(); for (int i=0; i<numChilds; i++) { selectChild(i); if (getAttribute("validSampleValues") == null) { final Double minValue = getAttributeAsDouble("minValue"); final Double maxValue = getAttributeAsDouble("maxValue"); if (minValue == null || maxValue == null || !(minValue <= maxValue)) { // Une '!' for catching NaN. /* * Stop the band scanning whatever happen: if a scan is recommended for at least * one band, do the scan. If we don't have float type, we don't need to continue * since this method will never returns 'true' in such case. */ return ImageUtilities.isFloatType(reader.getRawImageType(imageIndex).getSampleModel().getDataType()); } } } return false; } /** * Fixes the given value for rounding errors. This method should be invoked only for * variables related to sample dimensions, in order to avoid mixing potentially different * approach for fixing rounding error (the criterion for geographic coordinates could be * different). * * @param value The computed value. * @return The value to store. * * @since 3.16 */ public static double fixRoundingError(double value) { final double sv = value * 36000; final double sr = Math.rint(sv); if (sv != sr && Math.abs(sv - sr) <= EPS) { value = sr / 36000; } if (value == 0) { value = 0; // Replace negative zero by positive zero. } return value; } /** * Invokes {@link #fixRoundingError(double)} for all elements in the given array. * Values in the given array will be modified in-place, and the same array is * returned for convenience. * * @param values The array of values to fix for rounding error. * @return The given array, which now contains potentially modified values. * * @since 3.16 */ public static double[] fixRoundingError(final double[] values) { for (int i=0; i<values.length; i++) { values[i] = fixRoundingError(values[i]); } return values; } /** * Temporary Method in attempt to generalize comportment with reflexivity. * @param i * @return */ public GridSampleDimension getGridSampleDimension(int i) { selectParent(); selectChild(i); final Object userObject = getUserObject(); if (userObject != null && userObject instanceof GridSampleDimension) { return (GridSampleDimension) userObject; } return null; } /** * Extract SampleDimension form accessor. * * @return list of SampleDimension or null */ public List<GridSampleDimension> getGridSampleDimensions() { selectParent(); final Object userObj = getUserObject(); if (userObj != null && userObj instanceof List) return (List<GridSampleDimension>) userObj; final int nbC = childCount(); if (nbC == 0) return null; final List<GridSampleDimension> gsD = new ArrayList<>(nbC); for (int i = 0; i < nbC; i++) { selectChild(i); final Object obj = getUserObject(); if (obj != null) gsD.add((GridSampleDimension) obj); } return (gsD.isEmpty()) ? null : gsD; } }