/* * 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.image.io.metadata; import java.awt.Point; import java.util.List; import java.util.Locale; import java.util.ArrayList; import java.util.Objects; import java.text.NumberFormat; import java.text.FieldPosition; import java.awt.Rectangle; import java.awt.geom.Dimension2D; import java.awt.geom.AffineTransform; import javax.imageio.IIOParam; import javax.measure.Unit; import org.apache.sis.measure.UnitFormat; import org.opengis.geometry.DirectPosition; import org.opengis.util.FactoryException; import org.opengis.util.InternationalString; import org.opengis.coverage.grid.RectifiedGrid; import org.opengis.parameter.ParameterValueGroup; import org.opengis.referencing.cs.CoordinateSystem; import org.opengis.referencing.operation.Matrix; import org.opengis.referencing.operation.MathTransform; import org.opengis.referencing.operation.MathTransform1D; import org.opengis.referencing.operation.MathTransformFactory; import org.opengis.metadata.content.TransferFunctionType; import org.apache.sis.math.MathFunctions; import org.geotoolkit.resources.Errors; import org.geotoolkit.resources.Vocabulary; import org.apache.sis.util.Localized; import org.apache.sis.measure.NumberRange; import org.apache.sis.measure.MeasurementRange; import org.apache.sis.internal.util.UnmodifiableArrayList; import org.geotoolkit.internal.InternalUtilities; import org.geotoolkit.factory.FactoryFinder; import org.geotoolkit.coverage.Category; import org.geotoolkit.coverage.GridSampleDimension; import org.geotoolkit.display.shape.DoubleDimension2D; import org.geotoolkit.image.io.ImageMetadataException; import org.apache.sis.referencing.operation.matrix.Matrix2; import org.apache.sis.referencing.operation.matrix.Matrices; import static org.apache.sis.util.collection.Containers.isNullOrEmpty; /** * Utility methods extracting commonly used informations from ISO 19115-2 or ISO 19123 objects. * Instances of ISO 19115-2 metadata are typically obtained from {@link SpatialMetadata} objects. * See the <a href="SpatialMetadataFormat.html#default-formats">format description</a> for a * description of the expected metadata tree. * * @author Martin Desruisseaux (Geomatys) * @version 3.19 * * @since 3.07 * @module */ public class MetadataHelper implements Localized { /** * The default instance. */ public static final MetadataHelper INSTANCE = new MetadataHelper(null); /** * Small tolerance factor for comparisons of floating point numbers. */ private static final double EPS = 1E-10; /** * The image reader or writer for which we are creating metadata, or {@code null} if none. */ private final org.apache.sis.util.Localized owner; /** * The math transform factory, fetched only if needed. */ private transient MathTransformFactory mtFactory; /** * A math transform which, when used, is likely to be reused again. */ private transient MathTransform exponential; /** * Creates a new metadata helper for the given {@code ImageReader} or {@code ImageWriter}. * * @param owner The image reader or writer for which we are creating metadata, * or {@code null} if none. */ public MetadataHelper(final org.apache.sis.util.Localized owner) { this.owner = owner; } /** * Returns the locale used by this helper, or {@code null} for the default locale. This is * used for formatting text in methods like {@link #formatCellDimension formatCellDimension}, * and for localization of error messages when an exception is thrown. * * @return The locale, or {@code null} if unspecified. * * @since 3.09 */ @Override public Locale getLocale() { return (owner != null) ? owner.getLocale() : null; } /** * Returns the math transform factory. */ private MathTransformFactory getMathTransformFactory() { if (mtFactory == null) { mtFactory = FactoryFinder.getMathTransformFactory(null); } return mtFactory; } /** * Returns the error message from the given resource key and arguments. * The key shall be one of the {@link Errors.Key} constants. This is used * for formatting the message in {@link ImageMetadataException}. */ private String error(final short key, final Object... arguments) { return Errors.getResources(getLocale()).getString(key, arguments); } /** * Ensures that the given vector is non-null and non-empty. */ private void ensureVectorsExist(final List<?> vectors) throws ImageMetadataException { ensureMetadataExists("OffsetVectors", -1, vectors); if (vectors.isEmpty()) { throw new ImageMetadataException(error(Errors.Keys.NoParameterValue_1, "OffsetVectors")); } } /** * Ensures that the given value is non-null. The value is presumed extracted * from a metadata attribute. * * @param name The name of the metadata attribute. * @param index The index to append to {@code name}, or -1 if none. * @param value The value extracted from the metadata. * @throws ImageMetadataException If the given value is null. */ private void ensureMetadataExists(String name, int index, Object value) throws ImageMetadataException { if (value == null) { if (index >= 0) { name = name + '[' + index + ']'; } throw new ImageMetadataException(error(Errors.Keys.NoParameter_1, name)); } } /** * Ensures that the given {@code dimension} argument is equal to the expected value. * * @param name The name of the parameter being verified. * @param index The index to append to {@code name}, or -1 if none. * @param dimension The dimension which shall be equals to {@code expected}. * @param expected The expected dimension value (often 2). * @throws ImageMetadataException If the given dimension is not equals to {@code expected}. */ private void ensureDimensionMatch(String name, int index, int dimension, final int expected) throws ImageMetadataException { if (dimension != expected) { if (index >= 0) { name = name + '[' + index + ']'; } throw new ImageMetadataException(error(Errors.Keys.MismatchedDimension_3, name, dimension, expected)); } } /** * Returns the range of geophysics values defined in the given {@code SampleDimension} object. * This method tries to build the range from the * {@linkplain SampleDimension#getMinValue() minimum value}, * {@linkplain SampleDimension#getMaxValue() maximum value}, * {@linkplain SampleDimension#getScaleFactor() scale factor}, * {@linkplain SampleDimension#getOffset() offset} and the * {@linkplain SampleDimension#getFillSampleValues() fill sample values} metadata attributes. * * @param dimension The object from which to extract the range. * @return The range of geophysics values, or {@code null}. * * @since 3.08 */ public NumberRange<?> getValidValues(final SampleDimension dimension) { if (dimension == null) { return null; } return getSampleValues(dimension, dimension.getFillSampleValues(), true); } /** * Returns the range of sample values defined in the given {@code SampleDimension} object. This * method first looks at the value returned by {@link SampleDimension#getValidSampleValues()}. * If the later returns {@code null}, then this method tries to build the range from the * {@linkplain SampleDimension#getMinValue() minimum value}, * {@linkplain SampleDimension#getMaxValue() maximum value} and the * {@linkplain SampleDimension#getFillSampleValues() fill sample values} metadata attributes. * <p> * The fill sample values are used in order to determine if the minimum and maximum values * are inclusive or exclusive: if an extremum is equals to a fill sample value, then it is * considered exclusive. Otherwise it is considered inclusive. * * @param dimension The object from which to extract the range. * @return The range of sample values, or {@code null}. */ public NumberRange<?> getValidSampleValues(final SampleDimension dimension) { NumberRange<?> range = null; if (dimension != null) { range = dimension.getValidSampleValues(); if (range == null) { range = getSampleValues(dimension, dimension.getFillSampleValues(), false); } } return range; } /** * Returns the range of sample values defined in the given {@code SampleDimension} object. * This method performs the same work than {@link #getValidSampleValues(SampleDimension)}, * except that the band index and fill sample values are given explicitly. * Note that the fill sample values is not an ISO 19115-2 attribute. * <p> * This method is invoked by {@link #getSampleValues(SampleDimension, double[], boolean)}. * Subclasses can override it for forcing the usage of a different range of sample values. * * @param bandIndex Index of the band for which to get the valid sample values. * @param dimension The object from which to extract the range. * @param fillSampleValues The no-data values, or {@code null} if none. * @return The range of sample values, or {@code null}. */ public NumberRange<?> getValidSampleValues(final int bandIndex, final SampleDimension dimension, final double[] fillSampleValues) { NumberRange<?> range = null; if (dimension != null) { range = dimension.getValidSampleValues(); if (range == null) { range = getSampleValues(dimension, fillSampleValues, false); } } return range; } /** * Calculates the range of values. This is the range of geophysics values if * {@code geophysics} if {@code true}, or the range of sample values otherwise. */ private NumberRange<?> getSampleValues(final SampleDimension dimension, final double[] fillSampleValues, final boolean geophysics) { Double minimum = dimension.getMinValue(); Double maximum = dimension.getMaxValue(); boolean isMinInclusive = true; boolean isMaxInclusive = true; if (!geophysics || fillSampleValues != null) { Double sampleMin = minimum; Double sampleMax = maximum; Double n; final double scale = ((n = dimension.getScaleFactor()) != null) ? n : 1; final double offset = ((n = dimension.getOffset()) != null) ? n : 0; if (scale != 1 || offset != 0) { if (sampleMin != null) sampleMin = (sampleMin - offset) / scale; if (sampleMax != null) sampleMax = (sampleMax - offset) / scale; } if (fillSampleValues != null) { isMinInclusive = inclusive(sampleMin, fillSampleValues); isMaxInclusive = inclusive(sampleMax, fillSampleValues); } if (!geophysics) { minimum = sampleMin; maximum = sampleMax; } } if (geophysics) { final Unit<?> units = dimension.getUnits(); if (units != null) { return MeasurementRange.createBestFit(minimum, isMinInclusive, maximum, isMaxInclusive, units); } } return NumberRange.createBestFit(minimum, isMinInclusive, maximum, isMaxInclusive); } /** * Returns {@code true} if the given {@code nodataValues} array does <strong>not</strong> * contains the given value. In such case, the value can be considered inclusive. */ private static boolean inclusive(final Number value, final double[] nodataValues) { if (value != null) { final double n = value.doubleValue(); for (final double c : nodataValues) { if (c == n) { return false; } } } return true; } /** * Creates the <cite>Grid to CRS</cite> conversion from the {@linkplain RectifiedGrid#getOrigin() * origin} and {@linkplain RectifiedGrid#getOffsetVectors() offset vectors} of the given domain. * <p> * This method is similar to {@link #getAffineTransform(RectifiedGrid, IIOParam)}, except that * it is not restricted to a two-dimensional conversion and does not take an {@link IIOParam} * object in account. * * @param domain The domain from which to extract the origin and offset vectors. * @return The <cite>Grid to CRS</cite> conversion extracted from the given domain. * @throws ImageMetadataException If a mandatory attribute is missing from the given domain. * * @since 3.09 */ public MathTransform getGridToCRS(final RectifiedGrid domain) throws ImageMetadataException { final DirectPosition origin = domain.getOrigin(); ensureMetadataExists("origin", -1, origin); final List<double[]> vectors = domain.getOffsetVectors(); ensureVectorsExist(vectors); final int dimSource = vectors.size(); // Number of dimensions in the grid. final int dimTarget = origin.getDimension(); // Number of dimensions in the CRS. final Matrix matrix = Matrices.createDiagonal(dimTarget + 1, dimSource + 1); if (dimTarget < dimSource) { matrix.setElement(dimTarget, dimTarget, 0); } matrix.setElement(dimTarget, dimSource, 1); for (int i=0; i<dimSource; i++) { final double[] v = vectors.get(i); ensureMetadataExists("OffsetVector", i, v); ensureDimensionMatch("OffsetVector", i, v.length, dimTarget); for (int j=0; j<dimTarget; j++) { matrix.setElement(j, i, v[j]); } } for (int j=0; j<dimTarget; j++) { matrix.setElement(j, dimSource, origin.getOrdinate(j)); } final MathTransformFactory mtFactory = getMathTransformFactory(); try { return mtFactory.createAffineTransform(matrix); } catch (FactoryException e) { throw new ImageMetadataException(e); } } /** * Creates an affine transform from the {@linkplain RectifiedGrid#getOrigin() origin} and * {@linkplain RectifiedGrid#getOffsetVectors() offset vectors} of the given domain. If * the {@code param} parameter is non-null, then the affine transform is scaled and * translated according the subsampling, source and destination regions specified. * <p> * Note that the returned transform may maps pixel corner or pixel center, depending on the * value returned by {@link org.opengis.metadata.spatial.Georectified#getPointInPixel()}. * It is caller responsibility to make the necessary adjustments (tip: * {@link org.geotoolkit.metadata.iso.spatial.PixelTranslation} may be useful). * * @param domain The domain from which to extract the origin and offset vectors. * @param param Optional Image I/O parameters, or {@code null} if none. * @return The affine transform extracted from the given domain. * @throws ImageMetadataException If a mandatory attribute is missing from the given domain, * or if this method can not extract the two first dimensions from the domain. */ public AffineTransform getAffineTransform(final RectifiedGrid domain, final IIOParam param) throws ImageMetadataException { final List<double[]> vectors = domain.getOffsetVectors(); ensureVectorsExist(vectors); final int dimSource = vectors.size(); if (dimSource < 2) { ensureDimensionMatch("OffsetVectors", -1, dimSource, 2); // Exception always thrown. } final DirectPosition origin = domain.getOrigin(); ensureMetadataExists("origin", -1, origin); final int dimTarget = origin.getDimension(); if (dimTarget < 2 || !isSeparable(vectors)) { ensureDimensionMatch("origin", -1, dimTarget, 2); } final double[] matrix = new double[6]; for (int i=0; i<=1; i++) { final double[] v = vectors.get(i); ensureMetadataExists("OffsetVector", i, v); ensureDimensionMatch("OffsetVector", i, v.length, dimTarget); System.arraycopy(v, 0, matrix, i*2, 2); matrix[i+4] = origin.getOrdinate(i); } for (int i=0; i<matrix.length; i++) { matrix[i] = adjustForRoundingError(matrix[i]); } final AffineTransform tr = new AffineTransform(matrix); if (param != null) { final Rectangle source = param.getSourceRegion(); final Point target = param.getDestinationOffset(); if (target != null) { tr.translate(-target.x, -target.y); } tr.scale(param.getSourceXSubsampling(), param.getSourceYSubsampling()); if (source != null) { tr.translate(source.x + param.getSubsamplingXOffset(), source.y + param.getSubsamplingYOffset()); } } return tr; } /** * Returns {@code true} if the two first dimensions in the given array of vectors * are separable from all other dimensions. This is used in order to determine if * we can extract a two dimensional affine transform from the domain. */ private static boolean isSeparable(final List<double[]> vectors) { for (int i=vectors.size(); --i>=0;) { final double[] vector = vectors.get(i); if (vector != null) { int lower, upper; if (i >= 2) { lower = 0; upper = 2; } else { lower = 2; upper = vector.length; } while (lower < upper) { if (vector[lower++] != 0) { return false; } } } } return true; } /** * Returns the size of pixels, which must be square. The {@code gridToCRS} argument is * typically the output of {@link #getAffineTransform getAffineTransform}. This method * checks if the given transform complies with the following conditions: * <p> * <ul> * <li>The {@link AffineTransform#getScaleX() scaleX} coefficient must be * greater than zero.</li> * <li>The {@link AffineTransform#getScaleY() scaleY} coefficient must be * the negative value of {@code scaleX}, because the Y axis is assumed * reversed.</li> * <li>The {@link AffineTransform#getShearX() shearX} and {@link AffineTransform#getShearY() * shearY} coefficients must be zero.</li> * </ul> * <p> * If all those conditions are meet, then {@code scaleX} is returned. Otherwise an * exception is thrown. This behavior is convenient for code like the * {@linkplain org.geotoolkit.image.io.plugin.AsciiGridWriter ASCII Grid writer}, * which require square pixels as of format specification. * * @param gridToCRS The affine transform from which to extract the cell size. * @return The cell size as a positive and non-null value. * @throws ImageMetadataException If the affine transform does not comply with the above cited conditions. */ public double getCellSize(final AffineTransform gridToCRS) throws ImageMetadataException { final double size = gridToCRS.getScaleX(); if (size > 0) { final double tol = size * EPS; if (Math.abs(gridToCRS.getScaleY() + size) <= tol && Math.abs(gridToCRS.getShearX()) <= tol && Math.abs(gridToCRS.getShearY()) <= tol) { return size; } } throw new ImageMetadataException(error(Errors.Keys.PixelsNotSquareOrRotatedImage)); } /** * Returns the dimension of pixels, or {@code null} if not applicable. The {@code gridToCRS} * argument is typically the output of {@link #getAffineTransform getAffineTransform}. This * method checks if the given transform complies with the following conditions: * <p> * <ul> * <li>The {@link AffineTransform#getShearX() shearX} * and {@link AffineTransform#getShearY() shearY} coefficients are zero.</li> * </ul> * <p> * If this condition is meet, then {@code scaleX} and <strong>the negative value</strong> * of {@code scaleY} (because the Y axis is assumed reversed) are returned in a new * {@link Dimension2D} object. Otherwise {@code null} is returned. This behavior is * convenient for code like the {@linkplain org.geotoolkit.image.io.plugin.AsciiGridWriter * ASCII Grid writer}, which require square pixels unless some extensions are enabled for * rectangular pixel. * * @param gridToCRS The affine transform from which to extract the cell size. * @return The cell dimension, or {@code null} if the image is rotated. */ public Dimension2D getCellDimension(final AffineTransform gridToCRS) { final double dx = gridToCRS.getScaleX(); final double dy = -gridToCRS.getScaleY(); final double tol = Math.max(Math.abs(dx), Math.abs(dy)) * EPS; if (Math.abs(gridToCRS.getShearX()) <= tol && Math.abs(gridToCRS.getShearY()) <= tol) { return new DoubleDimension2D(dx, dy); } return null; } /** * Returns the dimension of pixels as a text, or {@code null} if none. This method computes * the dimension from the {@linkplain RectifiedGrid#getOffsetVectors() offset vectors} and * appends the axis units, if any. * * @param domain The domain from which to compute the cell dimensions. * @param cs The "real world" coordinate system, or {@code null} if unknown. * @return A text representation of the cell dimension, or {@code null} if there is no * offset vectors. * * @since 3.09 */ public String formatCellDimension(final RectifiedGrid domain, final CoordinateSystem cs) { final List<double[]> offsetVectors = domain.getOffsetVectors(); if (offsetVectors == null) { return null; } /* * Get the pixel sizes, and verify if they are the same for all axes. */ double minSize = Double.POSITIVE_INFINITY; final double[] sizes = new double[offsetVectors.size()]; for (int i=0; i<sizes.length; i++) { sizes[i] = adjustForRoundingError(MathFunctions.magnitude(offsetVectors.get(i))); final double as = Math.abs(sizes[i]); if (as < minSize) { minSize = as; } } boolean sameSize = true; for (int i=1; i<sizes.length; i++) { if (sizes[i-1] != sizes[i]) { sameSize = false; break; } } /* * Get the units, and verify if they are the same for all axes. */ boolean sameUnits = true; Unit<?>[] units = null; if (cs != null) { units = new Unit<?>[Math.min(sizes.length, cs.getDimension())]; for (int i=0; i<units.length; i++) { units[i] = cs.getAxis(i).getUnit(); } for (int i=1; i<units.length; i++) { if (!Objects.equals(units[i-1], units[i])) { sameUnits = false; break; } } } final Unit<?> commonUnit = (sameUnits && units != null && units.length != 0) ? units[0] : null; /* * Now format the string. */ final StringBuffer buffer = new StringBuffer(24); final Locale locale = getLocale(); final FieldPosition pos = new FieldPosition(0); final NumberFormat nf; final UnitFormat uf; if (locale != null) { nf = NumberFormat.getInstance(locale); uf = new UnitFormat(locale); } else { nf = NumberFormat.getInstance(); uf = new UnitFormat(Locale.getDefault()); } InternalUtilities.configure(nf, minSize, 9); if (sameSize && sameUnits) { if (sizes.length != 0) { nf.format(sizes[0], buffer, pos); } } else { if (commonUnit != null) { buffer.append('('); } boolean needsSeparator = false; for (int i=0; i<sizes.length; i++) { if (needsSeparator) { buffer.append(" × "); } needsSeparator = true; nf.format(sizes[i], buffer, pos); if (!sameUnits && units != null && i<units.length) { final Unit<?> unit = units[i]; if (unit != null) { uf.format(unit, buffer.append(' '), pos); } } } } if (commonUnit != null) { if (!sameSize) { buffer.append(')'); } uf.format(commonUnit, buffer.append(' '), pos); } return buffer.toString(); } /** * Converts the given {@link SampleDimension} instances to {@link GridSampleDimension} instances. * For each input sample dimension, this method creates a qualitative {@linkplain Category * category} for each {@linkplain SampleDimension#getFillSampleValues() fill values} (if any) * and a single quantitative category for the {@linkplain SampleDimension#getValidSampleValues() * range of sample values}. * <p> * The {@code sampleDimensions} argument is typically obtained by the following method call: * * {@preformat java * SpatialMetadata metadata = ... * sampleDimensions = metadata.getListForType(SampleDimension.class); * } * * @param sampleDimensions The sample dimensions from Image I/O metadata, or {@code null}. * @return The {@link GridSampleDimension}s, or {@code null} if the given list was null or empty. * @throws ImageMetadataException If this method can not create the grid sample dimensions. * * @since 3.13 */ public List<GridSampleDimension> getGridSampleDimensions( final List<? extends SampleDimension> sampleDimensions) throws ImageMetadataException { if (isNullOrEmpty(sampleDimensions)) { return null; } /* * Now convert the SampleDimension instances to GridSampleDimension instances. * For each sample dimension, we create a qualitative category for each fill * values (if any) and a single quantitative category for the range of sample * values. */ boolean allGeophysics = true; InternationalString untitled = null; // To be created only if needed. final List<Category> categories = new ArrayList<>(); final GridSampleDimension[] bands = new GridSampleDimension[sampleDimensions.size()]; boolean hasSampleDimensions = false; for (int i=0; i<bands.length; i++) { final SampleDimension sd = sampleDimensions.get(i); if (sd != null) { /* * Get a name for the sample dimensions. This name will be given both to the * GridSampleDimension object and to the single qualitative Category. If no * name can be found, "Untitled" will be used. */ InternationalString dimensionName = sd.getDescriptor(); if (dimensionName == null) { if (untitled == null) { untitled = Vocabulary.formatInternational(Vocabulary.Keys.Untitled); } dimensionName = untitled; } else { hasSampleDimensions = true; } /* * Create a qualitative category for each fill value (usually only one). * Those categories need to be created before the quantitative category, * because we will need this information in order to detect invalid range * of geophysics values. */ final double[] fillValues = sd.getFillSampleValues(); if (fillValues != null) { final CharSequence name = Category.NODATA.getName(); for (final double fv : fillValues) { final int ifv = (int) fv; final Category c; if (ifv == fv) { c = new Category(name, null, ifv); } else { c = new Category(name, null, fv); } categories.add(c); } } /* * Create a quantitative category for the range of valid sample values. * If there is no offset and scale factor, then the values are assumed * geophysics values (the scale is set to 1 and the offset to 0). */ final NumberRange<?> range = getValidSampleValues(i, sd, fillValues); if (range != null) { final Double scale = sd.getScaleFactor(); final Double offset = sd.getOffset(); final boolean isGeophysics = (scale == null && offset == null); if (!isGeophysics || !overlap(categories, range)) { final TransferFunctionType type = sd.getTransferFunctionType(); final MathTransformFactory mtFactory = getMathTransformFactory(); MathTransform tr; try { /* * NOTE: The formulas in this block must be consistent with the * formulas in CategoryTable.getCategories(String). */ tr = mtFactory.createAffineTransform(new Matrix2( (scale != null) ? adjustForRoundingError(scale) : 1, (offset != null) ? adjustForRoundingError(offset) : 0, 0, 1)); if (type != null && !type.equals(TransferFunctionType.LINEAR)) { if (type.equals(TransferFunctionType.EXPONENTIAL)) { if (exponential == null) { final ParameterValueGroup param = mtFactory.getDefaultParameters("Exponential"); param.parameter("base").setValue(10d); // Must be a 'double' exponential = mtFactory.createParameterizedTransform(param); } tr = mtFactory.createConcatenatedTransform(tr, exponential); } else { throw new ImageMetadataException(Errors.getResources(getLocale()) .getString(Errors.Keys.UnsupportedOperation_1, type)); } } } catch (FactoryException e) { throw new ImageMetadataException(e); } categories.add(new Category(dimensionName, null, range, (MathTransform1D) tr)); } allGeophysics &= isGeophysics; } /* * Create the GridSampleDimension instance. */ final Category[] array; if (categories.isEmpty()) { array = null; } else { array = categories.toArray(new Category[categories.size()]); hasSampleDimensions = true; } final Unit<?> unit = sd.getUnits(); if (unit != null) { hasSampleDimensions = true; } final GridSampleDimension band; try { band = new GridSampleDimension(dimensionName, array, unit); } catch (IllegalArgumentException e) { throw new ImageMetadataException(e); } bands[i] = band; categories.clear(); } } /* * At this point, we have all the sample dimensions. If the samples seem to be * already geophysics values, declare the bands as such. */ if (hasSampleDimensions) { for (int i=0; i<bands.length; i++) { bands[i] = bands[i].geophysics(allGeophysics); } return UnmodifiableArrayList.wrap(bands); } return null; } /** * Returns {@code true} if the given range overlaps at least one category. This method is * invoked when the image metadata declares a range of sample value, but does not declare * any offset and scale factor. Sometime (e.g. in some NetCDF files encoded in a way not * compliant with CF-convention), the range is actually garbage data that we should ignore. * In some other cases (e.g. in ASCII-Grid), the range is still valid. * <p> * As an heuristic rule, we will consider the range as garbage data if it overlaps any * previously defined categories. Attempt to create a {@code GridSampleDimension} with * such range would thrown an exception anyway. */ private static boolean overlap(final List<Category> categories, final NumberRange<?> range) { for (final Category category : categories) { if (range.intersectsAny(category.getRange())) { return true; } } return false; } /** * Works around the rounding errors found in some metadata numbers. We usually don't try to * "fix" rounding errors, but {@linkplain AffineTransform affine transform} coefficients are * an exception because they have a very deep impact on performance, especially the scale * factors: integer scales are often processed by optimized loops much faster than the loops * for fractional scales, and operations like matrix multiplications are more likely to produce * special cases like the {@linkplain AffineTransform#isIdentity() identity transform} when the * initial matrix coefficients have an exact IEEE 754 representation. * <p> * This method processes as below: * <p> * <ul> * <li>First, the given value is multiplied by 360. We choose the 360 value arbitrarily * because it is a multiple of many commonly used factors: 2, 3, 4, 5, 6, 10, 60 and * others.</li> * <li>If the result of the above step is almost an integer, * then round it, divide by 360 and return the result.</li> * <li>Otherwise return the given value unchanged (we do not return the result of * multiplication followed by a division, in order to avoid additional rounding * error).</li> * </ul> * * @param value The value that we want to adjust. * @return The adjusted value, or the given value unchanged if no adjustment were found. * * @see XAffineTransform#roundIfAlmostInteger(AffineTransform, double) */ public double adjustForRoundingError(final double value) { return InternalUtilities.adjustForRoundingError(value, 360, 16); // The above threshold (16) has been determined empirically from IFREMER data. } }