sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject svn commit: r1759429 - in /sis/branches/JDK8: core/sis-utility/src/main/java/org/apache/sis/util/resources/ storage/sis-earth-observation/src/main/java/org/apache/sis/storage/earthobservation/ storage/sis-earth-observation/src/main/java/org/apache/sis/...
Date Tue, 06 Sep 2016 12:53:46 GMT
Author: desruisseaux
Date: Tue Sep  6 12:53:45 2016
New Revision: 1759429

URL: http://svn.apache.org/viewvc?rev=1759429&view=rev
Log:
Complete information about bands from Landsat metadata.

Modified:
    sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.java
    sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.properties
    sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors_fr.properties
    sis/branches/JDK8/storage/sis-earth-observation/src/main/java/org/apache/sis/storage/earthobservation/LandsatReader.java
    sis/branches/JDK8/storage/sis-earth-observation/src/main/java/org/apache/sis/storage/earthobservation/doc-files/LandsatMetadata.html
    sis/branches/JDK8/storage/sis-earth-observation/src/test/java/org/apache/sis/storage/earthobservation/LandsatReaderTest.java
    sis/branches/JDK8/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/MetadataBuilder.java

Modified: sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.java?rev=1759429&r1=1759428&r2=1759429&view=diff
==============================================================================
--- sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.java [UTF-8] (original)
+++ sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.java [UTF-8] Tue Sep  6 12:53:45 2016
@@ -151,6 +151,11 @@ public final class Errors extends Indexe
         public static final short CanNotParseFile_2 = 8;
 
         /**
+         * Can not read property “{1}” in file “{0}”.
+         */
+        public static final short CanNotReadPropertyInFile_2 = 237;
+
+        /**
          * Can not read “{0}”.
          */
         public static final short CanNotRead_1 = 9;
@@ -336,11 +341,6 @@ public final class Errors extends Indexe
         public static final short ExcessiveStringSize = 153;
 
         /**
-         * A “{0}” statement was expected at line {2} or “{1}”.
-         */
-        public static final short ExpectedStatementAtLine_3 = 237;
-
-        /**
          * No factory of kind ‘{0}’ found.
          */
         public static final short FactoryNotFound_1 = 205;

Modified: sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.properties
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.properties?rev=1759429&r1=1759428&r2=1759429&view=diff
==============================================================================
--- sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.properties [ISO-8859-1] (original)
+++ sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.properties [ISO-8859-1] Tue Sep  6 12:53:45 2016
@@ -42,6 +42,7 @@ CanNotMapAxisToDirection_2        = Can
 CanNotOpen_1                      = Can not open \u201c{0}\u201d.
 CanNotParseFile_2                 = Can not parse \u201c{1}\u201d as a file in the {0} format.
 CanNotRead_1                      = Can not read \u201c{0}\u201d.
+CanNotReadPropertyInFile_2        = Can not read property \u201c{1}\u201d in file \u201c{0}\u201d.
 CanNotRepresentInFormat_2         = Can not represent \u201c{1}\u201d in a strictly standard-compliant {0} format.
 CanNotSeparateTargetDimension_1   = Target dimension {0} depends on excluded source dimensions.
 CanNotSetParameterValue_1         = Can not set a value for parameter \u201c{0}\u201d.
@@ -78,7 +79,6 @@ ExcessiveArgumentSize_3           = Argu
 ExcessiveListSize_2               = A size of {1} elements is excessive for the \u201c{0}\u201d list.
 ExcessiveNumberOfDimensions_1     = For this algorithm, {0} is an excessive number of dimensions.
 ExcessiveStringSize               = The character string is too long.
-ExpectedStatementAtLine_3         = A \u201c{0}\u201d statement was expected at line {2} or \u201c{1}\u201d.
 FactoryNotFound_1                 = No factory of kind \u2018{0}\u2019 found.
 FileNotFound_1                    = File \u201c{0}\u201d has not been found.
 ForbiddenAttribute_2              = Attribute \u201c{0}\u201d is not allowed for an object of type \u2018{1}\u2019.

Modified: sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors_fr.properties
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors_fr.properties?rev=1759429&r1=1759428&r2=1759429&view=diff
==============================================================================
--- sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors_fr.properties [ISO-8859-1] (original)
+++ sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors_fr.properties [ISO-8859-1] Tue Sep  6 12:53:45 2016
@@ -39,6 +39,7 @@ CanNotMapAxisToDirection_2        = Aucu
 CanNotOpen_1                      = Ne peut pas ouvrir \u00ab\u202f{0}\u202f\u00bb.
 CanNotParseFile_2                 = Ne peut pas lire \u00ab\u202f{1}\u202f\u00bb comme un fichier au format {0}.
 CanNotRead_1                      = Ne peut pas lire \u00ab\u202f{0}\u202f\u00bb.
+CanNotReadPropertyInFile_2        = Ne peut pas lire la propri\u00e9t\u00e9 \u00ab\u202f{1}\u202f\u00bb dans le fichier \u00ab\u202f{0}\u202f\u00bb.
 CanNotRepresentInFormat_2         = Ne peut pas repr\u00e9senter \u00ab\u202f{1}\u202f\u00bb dans un format {0} strictement conforme.
 CanNotSeparateTargetDimension_1   = La dimension de destination {0} d\u00e9pend de dimensions sources qui ont \u00e9t\u00e9 exclues.
 CanNotSetParameterValue_1         = Ne peut pas d\u00e9finir une valeur pour le param\u00e8tre \u00ab\u202f{0}\u202f\u00bb.
@@ -75,7 +76,6 @@ ExcessiveArgumentSize_3           = L\u2
 ExcessiveListSize_2               = Une taille de {1} \u00e9l\u00e9ments est excessive pour la liste \u00ab\u202f{0}\u202f\u00bb.
 ExcessiveNumberOfDimensions_1     = Pour cet algorithme, {0} est un trop grand nombre de dimensions.
 ExcessiveStringSize               = La cha\u00eene de caract\u00e8res est trop longue.
-ExpectedStatementAtLine_3         = Une d\u00e9claration \u00ab\u202f{0}\u202f\u00bb \u00e9tait attendue \u00e0 la ligne {2} de \u00ab\u202f{1}\u202f\u00bb.
 FactoryNotFound_1                 = Aucune fabrique de type \u2018{0}\u2019 n\u2019a \u00e9t\u00e9 trouv\u00e9e.
 FileNotFound_1                    = Le fichier \u00ab\u202f{0}\u202f\u00bb n\u2019a pas \u00e9t\u00e9 trouv\u00e9.
 ForbiddenAttribute_2              = L\u2019attribut \u00ab\u202f{0}\u202f\u00bb n\u2019est pas autoris\u00e9 pour un objet de type \u2018{1}\u2019.

Modified: sis/branches/JDK8/storage/sis-earth-observation/src/main/java/org/apache/sis/storage/earthobservation/LandsatReader.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/storage/sis-earth-observation/src/main/java/org/apache/sis/storage/earthobservation/LandsatReader.java?rev=1759429&r1=1759428&r2=1759429&view=diff
==============================================================================
--- sis/branches/JDK8/storage/sis-earth-observation/src/main/java/org/apache/sis/storage/earthobservation/LandsatReader.java [UTF-8] (original)
+++ sis/branches/JDK8/storage/sis-earth-observation/src/main/java/org/apache/sis/storage/earthobservation/LandsatReader.java [UTF-8] Tue Sep  6 12:53:45 2016
@@ -18,14 +18,14 @@ package org.apache.sis.storage.earthobse
 
 import java.io.BufferedReader;
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Date;
+import java.util.List;
 import java.util.Locale;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.io.LineNumberReader;
-import javax.measure.unit.Unit;
-import javax.measure.quantity.Length;
 import javax.measure.unit.SI;
 
 import org.opengis.metadata.Metadata;
@@ -33,10 +33,12 @@ import org.opengis.metadata.citation.Cit
 import org.opengis.metadata.citation.DateType;
 import org.opengis.metadata.content.ContentInformation;
 import org.opengis.metadata.content.CoverageContentType;
+import org.opengis.metadata.content.TransferFunctionType;
 import org.opengis.metadata.identification.Identification;
 import org.opengis.metadata.maintenance.ScopeCode;
 
 import org.apache.sis.metadata.iso.DefaultMetadata;
+import org.apache.sis.metadata.iso.DefaultIdentifier;
 import org.apache.sis.metadata.iso.citation.Citations;
 import org.apache.sis.metadata.iso.content.DefaultAttributeGroup;
 import org.apache.sis.metadata.iso.content.DefaultBand;
@@ -59,7 +61,6 @@ import java.time.OffsetDateTime;
 import java.time.OffsetTime;
 import java.time.DateTimeException;
 import java.time.temporal.Temporal;
-import org.opengis.metadata.content.AttributeGroup;
 
 
 /**
@@ -92,41 +93,34 @@ import org.opengis.metadata.content.Attr
  */
 final class LandsatReader {
     /**
-     * The description of all bands that can be included in a Landsat coverage.
-     * This description is hard-coded and shared by all metadata instances.
+     * Names of Landsat bands.
      *
-     * @todo Move those information in a database after we implemented the {@code org.apache.sis.metadata.sql} package.
+     * @see #bands
+     * @see #band(int)
      */
-    private static final AttributeGroup BANDS;
-    static {
-        final double[] wavelengths = {433, 482, 562, 655, 865, 1610, 2200, 590, 1375, 10800, 12000};
-        final String[] nameband = {
-            "Coastal Aerosol",                      //   433 nm
-            "Blue",                                 //   482 nm
-            "Green",                                //   562 nm
-            "Red",                                  //   655 nm
-            "Near-Infrared",                        //   865 nm
-            "Short Wavelength Infrared (SWIR) 1",   //  1610 nm
-            "Short Wavelength Infrared (SWIR) 2",   //  2200 nm
-            "Panchromatic",                         //   590 nm
-            "Cirrus",                               //  1375 nm
-            "Thermal Infrared Sensor (TIRS) 1",     // 10800 nm
-            "Thermal Infrared Sensor (TIRS) 2"      // 12000 nm
-        };
-        final DefaultBand[] bands = new DefaultBand[wavelengths.length];
-        final Unit<Length> nm = SI.MetricPrefix.NANO(SI.METRE);
-        for (int i = 0; i < bands.length; i++) {
-            final DefaultBand band = new DefaultBand();
-            band.setDescription(new SimpleInternationalString(nameband[i]));
-            band.setPeakResponse(wavelengths[i]);
-            band.setBoundUnits(nm);
-            bands[i] = band;
-        }
-        final DefaultAttributeGroup attributes = new DefaultAttributeGroup(CoverageContentType.PHYSICAL_MEASUREMENT, null);
-        attributes.setAttributes(Arrays.asList(bands));
-        attributes.freeze();
-        BANDS = attributes;
-    }
+    private static final String[] BAND_NAMES = {
+        "Coastal Aerosol",                      //   433 nm
+        "Blue",                                 //   482 nm
+        "Green",                                //   562 nm
+        "Red",                                  //   655 nm
+        "Near-Infrared",                        //   865 nm
+        "Short Wavelength Infrared (SWIR) 1",   //  1610 nm
+        "Short Wavelength Infrared (SWIR) 2",   //  2200 nm
+        "Panchromatic",                         //   590 nm
+        "Cirrus",                               //  1375 nm
+        "Thermal Infrared Sensor (TIRS) 1",     // 10800 nm
+        "Thermal Infrared Sensor (TIRS) 2"      // 12000 nm
+    };
+
+    /**
+     * Peak response wavelength for the Landsat bands, in nanometres.
+     *
+     * @see #bands
+     * @see #band(int)
+     */
+    private static final short[] WAVELENGTHS = {
+        433, 482, 562, 655, 865, 1610, 2200, 590, 1375, 10800, 12000
+    };
 
     /**
      * The pattern determining if the value of {@code ORIGIN} key is of the form
@@ -135,6 +129,28 @@ final class LandsatReader {
     static final Pattern CREDIT = Pattern.compile("\\bcourtesy\\h+of\\h+(the)?\\b\\s*", Pattern.CASE_INSENSITIVE);
 
     /**
+     * The {@value} suffix added to attribute names that are followed by a band number.
+     * This band suffix is itself followed by the {@code '_'} character, then the band number.
+     * Example: {@code "REFLECTANCE_ADD_BAND_1"}.
+     */
+    private static final String BAND_SUFFIX = "_BAND";
+
+    /**
+     * Index of panchromatic, reflective or thermal images in {@link #gridSize} array.
+     * Each kind of images have its size described by 2 integers: the width and the height.
+     */
+    private static final int PANCHROMATIC = 0,
+                             REFLECTIVE   = 2,
+                             THERMAL      = 4;
+
+    /**
+     * Index of projected and geographic coordinates in the {@link #corners} array.
+     * Each kind of coordinates are stored as 4 corners of 2 ordinate values.
+     */
+    private static final int PROJECTED  = 0,
+                             GEOGRAPHIC = 8;
+
+    /**
      * The keyword for end of metadata file.
      */
     private static final String END = "END";
@@ -184,6 +200,37 @@ final class LandsatReader {
     private final double[] corners;
 
     /**
+     * Image width and hight, in pixels. Values are (<var>width</var>,<var>height</var>) tuples.
+     * Tuples in this array are for {@link #PANCHROMATIC}, {@link #REFLECTIVE} or {@link #THERMAL}
+     * bands, in that order.
+     */
+    private final int[] gridSizes;
+
+    /**
+     * The bands description. Any element can be null if the corresponding band is not defined.
+     * The bands can be, in this exact order:
+     *
+     * <ol>
+     *   <li>Coastal Aerosol</li>
+     *   <li>Blue</li>
+     *   <li>Green</li>
+     *   <li>Red</li>
+     *   <li>Near-Infrared</li>
+     *   <li>Short Wavelength Infrared (SWIR) 1</li>
+     *   <li>Short Wavelength Infrared (SWIR) 2</li>
+     *   <li>Panchromatic</li>
+     *   <li>Cirrus</li>
+     *   <li>Thermal Infrared Sensor (TIRS) 1</li>
+     *   <li>Thermal Infrared Sensor (TIRS) 2</li>
+     * </ol>
+     *
+     * @see #BAND_NAMES
+     * @see #WAVELENGTHS
+     * @see #band(int)
+     */
+    private final DefaultBand[] bands;
+
+    /**
      * Creates a new metadata parser.
      *
      * @param  filename   an identifier of the file being read, or {@code null} if unknown.
@@ -195,7 +242,9 @@ final class LandsatReader {
         this.locale    = locale;
         this.listeners = listeners;
         this.metadata  = new MetadataBuilder();
-        this.corners   = new double[16];
+        this.bands     = new DefaultBand[BAND_NAMES.length];
+        this.gridSizes = new int[THERMAL + 2];                  // THERMAL is the last group of images grid size.
+        this.corners   = new double[GEOGRAPHIC + 8];            // GEOGRAPHIC is the last group of corners to store.
         Arrays.fill(corners, Double.NaN);
     }
 
@@ -225,58 +274,85 @@ final class LandsatReader {
                     /*
                      * Landsat metadata ends with the END keyword, without value after that keyword.
                      * If we find it, stop reading. All remaining lines (if any) will be ignored.
-                     * If a group was opened but not closed, report a warning.
                      */
                     if (end - start != END.length() || !line.regionMatches(true, start, END, 0, END.length())) {
                         throw new DataStoreException(errors().getString(Errors.Keys.NotAKeyValuePair_1, line));
                     }
-                    if (group != null) {
-                        missingEndGroup(reader);
-                    }
                     return;
-                } else {
-                    final String key = line.substring(start,
-                            CharSequences.skipTrailingWhitespaces(line, start, separator)).toUpperCase(Locale.US);
-                    start = CharSequences.skipLeadingWhitespaces(line, separator + 1, end);
-                    /*
-                     * In a Landsat file, String values are between quotes. Example: STATION_ID = "LGN".
-                     * If such quotes are found, remove them.
-                     */
-                    if (end - start >= 2 && line.charAt(start) == '"' && line.charAt(end - 1) == '"') {
-                        start = CharSequences.skipLeadingWhitespaces(line, start + 1, --end);
-                        end = CharSequences.skipTrailingWhitespaces(line, start, end);
-                    }
-                    try {
-                        parseKeyValuePair(key, line.substring(start, end), reader);
-                    } catch (IllegalArgumentException | DateTimeException e) {
-                        warning(e);
+                }
+                /*
+                 * If the key ends with "_BAND_" followed by a number, remove the band number from the
+                 * key and parse that number as an integer value. Exemple: "REFLECTANCE_ADD_BAND_1".
+                 * We keep the "_BAND_" suffix in the key for avoiding ambiguity.
+                 */
+                String key = line.substring(start, CharSequences.skipTrailingWhitespaces(line, start, separator)).toUpperCase(Locale.US);
+                int band = 0;
+                for (int i=key.length(); --i >= 0;) {
+                    final char c = key.charAt(i);
+                    if (c < '0' || c > '9') {
+                        if (c == '_') {
+                            if (key.regionMatches(i - BAND_SUFFIX.length(), BAND_SUFFIX, 0, BAND_SUFFIX.length())) try {
+                                band = Integer.parseInt(key.substring(++i));
+                                key = key.substring(0, i);
+                            } catch (NumberFormatException e) {
+                                warning(key, reader, e);
+                            }
+                        }
+                        break;
                     }
                 }
+                /*
+                 * In a Landsat file, String values are between quotes. Example: STATION_ID = "LGN".
+                 * If such quotes are found, remove them.
+                 */
+                start = CharSequences.skipLeadingWhitespaces(line, separator + 1, end);
+                if (end - start >= 2 && line.charAt(start) == '"' && line.charAt(end - 1) == '"') {
+                    start = CharSequences.skipLeadingWhitespaces(line, start + 1, --end);
+                    end = CharSequences.skipTrailingWhitespaces(line, start, end);
+                }
+                try {
+                    parseKeyValuePair(key, band, line.substring(start, end));
+                } catch (IllegalArgumentException | DateTimeException e) {
+                    warning(key, reader, e);
+                }
             }
         }
-        warning(Errors.Keys.UnexpectedEndOfFile_1, getFilename());
+        listeners.warning(errors().getString(Errors.Keys.UnexpectedEndOfFile_1, getFilename()), null);
     }
 
     /**
-     * Parses the given value and stores its value at the given index in the {@link #corners} array.
+     * Parses the given value and stores it at the given index in the {@link #corners} array.
+     * The given index must be one of the {@link #PROJECTED} or {@link #GEOGRAPHIC} constants
+     * plus the corner index.
      */
     private void parseCorner(final int index, final String value) throws NumberFormatException {
         corners[index] = Double.parseDouble(value);
     }
 
     /**
+     * Parses the given value and stores it at the given index in the {@link #gridSizes} array.
+     *
+     * @param  index  {@link #PANCHROMATIC}, {@link #REFLECTIVE} or {@link #THERMAL},
+     *                plus one for parsing the height instead than the width.
+     * @param  value  the value to parse.
+     */
+    private void parseGridSize(final int index, final String value) throws NumberFormatException {
+        gridSizes[index] = Integer.parseInt(value);
+    }
+
+    /**
      * Invoked for every key-value pairs found in the file.
      * Leading and trailing spaces, if any, have been removed.
      *
      * @param  key     the key in upper cases.
+     * @param  band    the band number, or 0 if none.
      * @param  value   the value, without quotes if those quotes existed.
-     * @param  reader  used only for reporting line number of warnings, if any.
      * @throws NumberFormatException if the value was expected to be a string but the parsing failed.
      * @throws DateTimeException if the value was expected to be a date but the parsing failed,
      *         or if the result of the parsing was not of the expected type.
      * @throws IllegalArgumentException if the value is out of range.
      */
-    private void parseKeyValuePair(final String key, final String value, final BufferedReader reader)
+    private void parseKeyValuePair(final String key, final int band, final String value)
             throws IllegalArgumentException, DateTimeException
     {
         switch (key) {
@@ -285,9 +361,6 @@ final class LandsatReader {
                 break;
             }
             case "END_GROUP": {
-                if (!value.equals(group) && group != null) {
-                    missingEndGroup(reader);
-                }
                 group = null;
                 break;
             }
@@ -420,14 +493,57 @@ final class LandsatReader {
              * Positive latitude value indicates north latitude; negative value indicates south latitude.
              * Units are in degrees.
              */
-            case "CORNER_UL_LON_PRODUCT": parseCorner( 8, value); break;
-            case "CORNER_UL_LAT_PRODUCT": parseCorner( 9, value); break;
-            case "CORNER_UR_LON_PRODUCT": parseCorner(10, value); break;
-            case "CORNER_UR_LAT_PRODUCT": parseCorner(11, value); break;
-            case "CORNER_LL_LON_PRODUCT": parseCorner(12, value); break;
-            case "CORNER_LL_LAT_PRODUCT": parseCorner(13, value); break;
-            case "CORNER_LR_LON_PRODUCT": parseCorner(14, value); break;
-            case "CORNER_LR_LAT_PRODUCT": parseCorner(15, value); break;
+            case "CORNER_UL_LON_PRODUCT": parseCorner(GEOGRAPHIC + 0, value); break;
+            case "CORNER_UL_LAT_PRODUCT": parseCorner(GEOGRAPHIC + 1, value); break;
+            case "CORNER_UR_LON_PRODUCT": parseCorner(GEOGRAPHIC + 2, value); break;
+            case "CORNER_UR_LAT_PRODUCT": parseCorner(GEOGRAPHIC + 3, value); break;
+            case "CORNER_LL_LON_PRODUCT": parseCorner(GEOGRAPHIC + 4, value); break;
+            case "CORNER_LL_LAT_PRODUCT": parseCorner(GEOGRAPHIC + 5, value); break;
+            case "CORNER_LR_LON_PRODUCT": parseCorner(GEOGRAPHIC + 6, value); break;
+            case "CORNER_LR_LAT_PRODUCT": parseCorner(GEOGRAPHIC + 7, value); break;
+            /*
+             * The upper-left (UL), upper-right (UR), lower-left (LL) and lower-right (LR) corner map
+             * projection X and Y coordinate, measured at the center of the pixel. Units are in meters.
+             */
+            case "CORNER_UL_PROJECTION_X_PRODUCT": parseCorner(PROJECTED + 0, value); break;
+            case "CORNER_UL_PROJECTION_Y_PRODUCT": parseCorner(PROJECTED + 1, value); break;
+            case "CORNER_UR_PROJECTION_X_PRODUCT": parseCorner(PROJECTED + 2, value); break;
+            case "CORNER_UR_PROJECTION_Y_PRODUCT": parseCorner(PROJECTED + 3, value); break;
+            case "CORNER_LL_PROJECTION_X_PRODUCT": parseCorner(PROJECTED + 4, value); break;
+            case "CORNER_LL_PROJECTION_Y_PRODUCT": parseCorner(PROJECTED + 5, value); break;
+            case "CORNER_LR_PROJECTION_X_PRODUCT": parseCorner(PROJECTED + 6, value); break;
+            case "CORNER_LR_PROJECTION_Y_PRODUCT": parseCorner(PROJECTED + 7, value); break;
+            /*
+             * The number of product lines and samples for the panchromatic, reflective and thermal bands.
+             * Those parameters are only present if the corresponding band is present in the product.
+             */
+            case "PANCHROMATIC_LINES":   parseGridSize(PANCHROMATIC + 1, value); break;
+            case "PANCHROMATIC_SAMPLES": parseGridSize(PANCHROMATIC,     value); break;
+            case "REFLECTIVE_LINES":     parseGridSize(REFLECTIVE + 1,   value); break;
+            case "REFLECTIVE_SAMPLES":   parseGridSize(REFLECTIVE,       value); break;
+            case "THERMAL_LINES":        parseGridSize(THERMAL + 1,      value); break;
+            case "THERMAL_SAMPLES":      parseGridSize(THERMAL,          value); break;
+            /*
+             * The grid cell size in meters used in creating the image for the band, if part of the product.
+             * This parameter is only included if the corresponding band is included in the product.
+             */
+            case "GRID_CELL_SIZE_PANCHROMATIC":
+            case "GRID_CELL_SIZE_REFLECTIVE":
+            case "GRID_CELL_SIZE_THERMAL": {
+                metadata.addResolution(Double.parseDouble(value));
+                break;
+            }
+            /*
+             * The file name for a band. This parameter is only present if the band is included in the product.
+             * We
+             */
+            case "FILE_NAME_BAND_": {
+                final DefaultBand db = band(key, band);
+                if (db != null) {
+                    db.getNames().add(new DefaultIdentifier(value));
+                }
+                break;
+            }
             /*
              * The file name for L1 metadata.
              * Exemple: "LC81230522014071LGN00_MTL.txt".
@@ -472,7 +588,101 @@ final class LandsatReader {
                 metadata.setIlluminationElevationAngle(Double.parseDouble(value));
                 break;
             }
+
+            ////
+            //// GROUP = MIN_MAX_PIXEL_VALUE
+            ////
+
+            /*
+             * Minimum achievable spectral radiance value for a band 1.
+             * This parameter is only present if this band is included in the product.
+             */
+            case "QUANTIZE_CAL_MIN_BAND_": {
+                final Double v = metadata.parseDouble(value);       // Done first in case an exception is thrown.
+                final DefaultBand db = band(key, band);
+                if (db != null) {
+                    db.setMinValue(v);
+                }
+                break;
+            }
+            /*
+             * Maximum achievable spectral radiance value for a band 1.
+             * This parameter is only present if this band is included in the product.
+             */
+            case "QUANTIZE_CAL_MAX_BAND_": {
+                final Double v = metadata.parseDouble(value);       // Done first in case an exception is thrown.
+                final DefaultBand db = band(key, band);
+                if (db != null) {
+                    db.setMaxValue(v);
+                }
+                break;
+            }
+
+            ////
+            //// GROUP = RADIOMETRIC_RESCALING
+            ////
+
+            /*
+             * The multiplicative rescaling factor used to convert calibrated DN to Radiance units for a band.
+             * Unit is W/(m² sr um)/DN.
+             */
+            case "RADIANCE_MULT_BAND_": {
+                setTransferFunction(key, band, true, value);
+                break;
+            }
+            /*
+             * The additive rescaling factor used to convert calibrated DN to Radiance units for a band.
+             * Unit is W/(m² sr um)/DN.
+             */
+            case "RADIANCE_ADD_BAND_": {
+                setTransferFunction(key, band, false, value);
+                break;
+            }
+        }
+    }
+
+    /**
+     * Sets a component of the linear transfer function.
+     *
+     * @param  key      the key without its band number. Used only for formatting warning messages.
+     * @param  band     index of the band to set.
+     * @param  isScale  {@code true} for setting the scale factor, or {@code false} for setting the offset.
+     * @param  value    the value to set.
+     */
+    private void setTransferFunction(final String key, final int band, final boolean isScale, final String value) {
+        final Double v = metadata.parseDouble(value);       // Done first in case an exception is thrown.
+        final DefaultBand db = band(key, band);
+        if (db != null) {
+            db.setTransferFunctionType(TransferFunctionType.LINEAR);
+            if (isScale) {
+                db.setScaleFactor(v);
+            } else {
+                db.setOffset(v);
+            }
+        }
+    }
+
+    /**
+     * Returns the band at the given index, creating it if needed.
+     * If the given index is out of range, then this method logs a warning and returns {@code null}.
+     *
+     * @param  key    the key without its band number. Used only for formatting warning messages.
+     * @param  index  the band index.
+     */
+    private DefaultBand band(final String key, int index) {
+        if (index < 1 || index > BAND_NAMES.length) {
+            listeners.warning(errors().getString(Errors.Keys.UnexpectedValueInElement_2, key + index, index), null);
+            return null;
+        }
+        DefaultBand band = bands[--index];
+        if (band == null) {
+            band = new DefaultBand();
+            band.setDescription(new SimpleInternationalString(BAND_NAMES[index]));
+            band.setPeakResponse((double) WAVELENGTHS[index]);
+            band.setBoundUnits(SI.MetricPrefix.NANO(SI.METRE));
+            bands[index] = band;
         }
+        return band;
     }
 
     /**
@@ -491,7 +701,7 @@ final class LandsatReader {
                 metadata.addExtent(t, t);
             } catch (UnsupportedOperationException e) {
                 // May happen if the temporal module (which is optional) is not on the classpath.
-                warning(e);
+                warning(null, null, e);
             }
         }
     }
@@ -508,7 +718,7 @@ final class LandsatReader {
         double ymin = Double.POSITIVE_INFINITY;
         double xmax = Double.NEGATIVE_INFINITY;
         double ymax = Double.NEGATIVE_INFINITY;
-        for (int i = base+8; --i >= 0;) {
+        for (int i = base+8; --i >= base;) {
             double v = corners[i];
             if (v < ymin) ymin = v;
             if (v > ymax) ymax = v;
@@ -537,10 +747,10 @@ final class LandsatReader {
             flushSceneTime();
         } catch (DateTimeException e) {
             // May happen if the SCENE_CENTER_TIME attribute was found without DATE_ACQUIRED.
-            warning(e);
+            warning(null, null, e);
         }
-        if (toBoundingBox(8)) {
-            metadata.addExtent(corners, 8);
+        if (toBoundingBox(GEOGRAPHIC)) {
+            metadata.addExtent(corners, GEOGRAPHIC);
         }
         final DefaultMetadata result = metadata.build(false);
         if (result != null) {
@@ -557,11 +767,17 @@ final class LandsatReader {
                 }
             }
             /*
-             * Set pre-defined information about all bands.
+             * Set information about all non-null bands.
              */
             final ContentInformation content = singletonOrNull(result.getContentInfo());
             if (content instanceof DefaultCoverageDescription) {
-                ((DefaultCoverageDescription) content).setAttributeGroups(singleton(BANDS));
+                final DefaultAttributeGroup attributes = new DefaultAttributeGroup(CoverageContentType.PHYSICAL_MEASUREMENT, null);
+                final List<DefaultBand> nonNulls = new ArrayList<>(bands.length);
+                for (final DefaultBand b : bands) {
+                    if (b != null) nonNulls.add(b);
+                }
+                attributes.setAttributes(nonNulls);
+                ((DefaultCoverageDescription) content).setAttributeGroups(singleton(attributes));
             }
             result.setMetadataStandards(Citations.ISO_19115);
             result.freeze();
@@ -577,30 +793,29 @@ final class LandsatReader {
     }
 
     /**
-     * Invoked when a non-fatal exception occurred while reading metadata. This method
-     * sends a record to the registered listeners if any, or logs the record otherwise.
+     * Prepends the group name before the given key, if a group name exists.
+     * This is used only for formatting warning messages.
      */
-    private void warning(final Exception e) {
-        listeners.warning(null, e);
+    private String toLongName(String key) {
+        if (group != null) {
+            key = group + ':' + key;
+        }
+        return key;
     }
 
     /**
-     * Invoked when a non-fatal error occurred while reading metadata. This method
+     * Invoked when a non-fatal exception occurred while reading metadata. This method
      * sends a record to the registered listeners if any, or logs the record otherwise.
-     *
-     * @param  error     one of the {@link Errors.Keys} values.
-     * @param  argument  the argument (or an array of arguments) to format.
      */
-    private void warning(final short error, final Object argument) {
-        listeners.warning(errors().getString(error, argument), null);
-    }
-
-    /**
-     * Invoked when an expected {@code END_GROUP} statement is missing.
-     */
-    private void missingEndGroup(final BufferedReader reader) {
-        final Object line = (reader instanceof LineNumberReader) ? ((LineNumberReader) reader).getLineNumber() : "?";
-        listeners.warning(errors().getString(Errors.Keys.ExpectedStatementAtLine_3, "END_GROUP " + group, getFilename(), line), null);
+    private void warning(String key, final BufferedReader reader ,final Exception e) {
+        if (key != null) {
+            String file = getFilename();
+            if (reader instanceof LineNumberReader) {
+                file = file + ":" + ((LineNumberReader) reader).getLineNumber();
+            }
+            key = errors().getString(Errors.Keys.CanNotReadPropertyInFile_2, toLongName(key), file);
+        }
+        listeners.warning(key, e);
     }
 
     /**

Modified: sis/branches/JDK8/storage/sis-earth-observation/src/main/java/org/apache/sis/storage/earthobservation/doc-files/LandsatMetadata.html
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/storage/sis-earth-observation/src/main/java/org/apache/sis/storage/earthobservation/doc-files/LandsatMetadata.html?rev=1759429&r1=1759428&r2=1759429&view=diff
==============================================================================
--- sis/branches/JDK8/storage/sis-earth-observation/src/main/java/org/apache/sis/storage/earthobservation/doc-files/LandsatMetadata.html (original)
+++ sis/branches/JDK8/storage/sis-earth-observation/src/main/java/org/apache/sis/storage/earthobservation/doc-files/LandsatMetadata.html Tue Sep  6 12:53:45 2016
@@ -120,6 +120,9 @@
       <tr><td><code>  │   │   └─</code>Format specification citation</td> <td></td>                                             <td></td></tr>
       <tr><td><code>  │   │       ├─</code>Title:</td>                    <td></td>                                             <td>Long name inferred from <code>OUTPUT_FORMAT</code> if possible.</td></tr>
       <tr><td><code>  │   │       └─</code>Alternate title:</td>          <td><code class="rf">OUTPUT_FORMAT</code></td>        <td>The name of the data transfer format, considered as an abbreviation.</td></tr>
+      <tr><td><code>  │   ├─</code>Spatial resolution (1 of 3)</td>       <td><code class="rf">GRID_CELL_SIZE_PANCHROMATIC</code></td><td>The grid cell size in meters for the panchromatic band, if part of the product.</td></tr>
+      <tr><td><code>  │   ├─</code>Spatial resolution (2 of 3)</td>       <td><code class="rf">GRID_CELL_SIZE_REFLECTIVE</code></td><td>The grid cell size in meters for the reflective bands, if part of the product.</td></tr>
+      <tr><td><code>  │   ├─</code>Spatial resolution (3 of 3)</td>       <td><code class="rf">GRID_CELL_SIZE_THERMAL</code></td><td>The grid cell size in meters for the thermal bands, if part of the product.</td></tr>
       <tr><td><code>  │   └─</code>Extent</td>                            <td></td>                                             <td></td></tr>
       <tr><td><code>  │       ├─</code>Geographic element</td>            <td></td>                                             <td></td></tr>
       <tr><td><code>  │       │   ├─</code>West bound longitude:</td>     <td><code>min(<span class="rf">CORNER_*_LON_PRODUCT</span>)</code></td> <td>The western-most longitude value in degrees (approximative).</td></tr>

Modified: sis/branches/JDK8/storage/sis-earth-observation/src/test/java/org/apache/sis/storage/earthobservation/LandsatReaderTest.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/storage/sis-earth-observation/src/test/java/org/apache/sis/storage/earthobservation/LandsatReaderTest.java?rev=1759429&r1=1759428&r2=1759429&view=diff
==============================================================================
--- sis/branches/JDK8/storage/sis-earth-observation/src/test/java/org/apache/sis/storage/earthobservation/LandsatReaderTest.java [UTF-8] (original)
+++ sis/branches/JDK8/storage/sis-earth-observation/src/test/java/org/apache/sis/storage/earthobservation/LandsatReaderTest.java [UTF-8] Tue Sep  6 12:53:45 2016
@@ -89,6 +89,10 @@ public class LandsatReaderTest extends T
                 + "  │   │   └─Format specification citation\n"
                 + "  │   │       ├─Title…………………………………………………… GeoTIFF Coverage Encoding Profile\n"
                 + "  │   │       └─Alternate title………………………… GEOTIFF\n"
+                + "  │   ├─Spatial resolution (1 of 2)\n"
+                + "  │   │   └─Distance……………………………………………………… 15.0\n"
+                + "  │   ├─Spatial resolution (2 of 2)\n"
+                + "  │   │   └─Distance……………………………………………………… 30.0\n"
                 + "  │   └─Extent\n"
                 + "  │       └─Geographic element\n"
                 + "  │           ├─West bound longitude…………… 108°20′24″E\n"
@@ -103,49 +107,126 @@ public class LandsatReaderTest extends T
                 + "  │   └─Attribute group\n"
                 + "  │       ├─Content type…………………………………………… Physical measurement\n"
                 + "  │       ├─Attribute (1 of 11)\n"
+                + "  │       │   ├─Max value………………………………………… 65535.0\n"
+                + "  │       │   ├─Min value………………………………………… 1.0\n"
                 + "  │       │   ├─Peak response……………………………… 433.0\n"
-                + "  │       │   ├─Bound units…………………………………… nm\n"
-                + "  │       │   └─Description…………………………………… Coastal Aerosol\n"
+                + "  │       │   ├─Scale factor………………………………… 0.0127\n"
+                + "  │       │   ├─Offset………………………………………………… -63.6\n"
+                + "  │       │   ├─Transfer function type……… Linear\n"
+                + "  │       │   ├─Bound units…………………………………… nm\n"
+                + "  │       │   ├─Description…………………………………… Coastal Aerosol\n"
+                + "  │       │   └─Name\n"
+                + "  │       │       └─Code…………………………………………… TestImage_B1.TIF\n"
                 + "  │       ├─Attribute (2 of 11)\n"
+                + "  │       │   ├─Max value………………………………………… 65535.0\n"
+                + "  │       │   ├─Min value………………………………………… 1.0\n"
                 + "  │       │   ├─Peak response……………………………… 482.0\n"
-                + "  │       │   ├─Bound units…………………………………… nm\n"
-                + "  │       │   └─Description…………………………………… Blue\n"
+                + "  │       │   ├─Scale factor………………………………… 0.013\n"
+                + "  │       │   ├─Offset………………………………………………… -65.1\n"
+                + "  │       │   ├─Transfer function type……… Linear\n"
+                + "  │       │   ├─Bound units…………………………………… nm\n"
+                + "  │       │   ├─Description…………………………………… Blue\n"
+                + "  │       │   └─Name\n"
+                + "  │       │       └─Code…………………………………………… TestImage_B2.TIF\n"
                 + "  │       ├─Attribute (3 of 11)\n"
+                + "  │       │   ├─Max value………………………………………… 65535.0\n"
+                + "  │       │   ├─Min value………………………………………… 1.0\n"
                 + "  │       │   ├─Peak response……………………………… 562.0\n"
-                + "  │       │   ├─Bound units…………………………………… nm\n"
-                + "  │       │   └─Description…………………………………… Green\n"
+                + "  │       │   ├─Scale factor………………………………… 0.012\n"
+                + "  │       │   ├─Offset………………………………………………… -60.0\n"
+                + "  │       │   ├─Transfer function type……… Linear\n"
+                + "  │       │   ├─Bound units…………………………………… nm\n"
+                + "  │       │   ├─Description…………………………………… Green\n"
+                + "  │       │   └─Name\n"
+                + "  │       │       └─Code…………………………………………… TestImage_B3.TIF\n"
                 + "  │       ├─Attribute (4 of 11)\n"
+                + "  │       │   ├─Max value………………………………………… 65535.0\n"
+                + "  │       │   ├─Min value………………………………………… 1.0\n"
                 + "  │       │   ├─Peak response……………………………… 655.0\n"
-                + "  │       │   ├─Bound units…………………………………… nm\n"
-                + "  │       │   └─Description…………………………………… Red\n"
+                + "  │       │   ├─Scale factor………………………………… 0.0101\n"
+                + "  │       │   ├─Offset………………………………………………… -50.6\n"
+                + "  │       │   ├─Transfer function type……… Linear\n"
+                + "  │       │   ├─Bound units…………………………………… nm\n"
+                + "  │       │   ├─Description…………………………………… Red\n"
+                + "  │       │   └─Name\n"
+                + "  │       │       └─Code…………………………………………… TestImage_B4.TIF\n"
                 + "  │       ├─Attribute (5 of 11)\n"
+                + "  │       │   ├─Max value………………………………………… 65535.0\n"
+                + "  │       │   ├─Min value………………………………………… 1.0\n"
                 + "  │       │   ├─Peak response……………………………… 865.0\n"
-                + "  │       │   ├─Bound units…………………………………… nm\n"
-                + "  │       │   └─Description…………………………………… Near-Infrared\n"
+                + "  │       │   ├─Scale factor………………………………… 0.00619\n"
+                + "  │       │   ├─Offset………………………………………………… -31.0\n"
+                + "  │       │   ├─Transfer function type……… Linear\n"
+                + "  │       │   ├─Bound units…………………………………… nm\n"
+                + "  │       │   ├─Description…………………………………… Near-Infrared\n"
+                + "  │       │   └─Name\n"
+                + "  │       │       └─Code…………………………………………… TestImage_B5.TIF\n"
                 + "  │       ├─Attribute (6 of 11)\n"
+                + "  │       │   ├─Max value………………………………………… 65535.0\n"
+                + "  │       │   ├─Min value………………………………………… 1.0\n"
                 + "  │       │   ├─Peak response……………………………… 1610.0\n"
-                + "  │       │   ├─Bound units…………………………………… nm\n"
-                + "  │       │   └─Description…………………………………… Short Wavelength Infrared (SWIR) 1\n"
+                + "  │       │   ├─Scale factor………………………………… 0.00154\n"
+                + "  │       │   ├─Offset………………………………………………… -7.7\n"
+                + "  │       │   ├─Transfer function type……… Linear\n"
+                + "  │       │   ├─Bound units…………………………………… nm\n"
+                + "  │       │   ├─Description…………………………………… Short Wavelength Infrared (SWIR) 1\n"
+                + "  │       │   └─Name\n"
+                + "  │       │       └─Code…………………………………………… TestImage_B6.TIF\n"
                 + "  │       ├─Attribute (7 of 11)\n"
+                + "  │       │   ├─Max value………………………………………… 65535.0\n"
+                + "  │       │   ├─Min value………………………………………… 1.0\n"
                 + "  │       │   ├─Peak response……………………………… 2200.0\n"
-                + "  │       │   ├─Bound units…………………………………… nm\n"
-                + "  │       │   └─Description…………………………………… Short Wavelength Infrared (SWIR) 2\n"
+                + "  │       │   ├─Scale factor………………………………… 5.19E-4\n"
+                + "  │       │   ├─Offset………………………………………………… -2.6\n"
+                + "  │       │   ├─Transfer function type……… Linear\n"
+                + "  │       │   ├─Bound units…………………………………… nm\n"
+                + "  │       │   ├─Description…………………………………… Short Wavelength Infrared (SWIR) 2\n"
+                + "  │       │   └─Name\n"
+                + "  │       │       └─Code…………………………………………… TestImage_B7.TIF\n"
                 + "  │       ├─Attribute (8 of 11)\n"
+                + "  │       │   ├─Max value………………………………………… 65535.0\n"
+                + "  │       │   ├─Min value………………………………………… 1.0\n"
                 + "  │       │   ├─Peak response……………………………… 590.0\n"
-                + "  │       │   ├─Bound units…………………………………… nm\n"
-                + "  │       │   └─Description…………………………………… Panchromatic\n"
+                + "  │       │   ├─Scale factor………………………………… 0.0115\n"
+                + "  │       │   ├─Offset………………………………………………… -57.3\n"
+                + "  │       │   ├─Transfer function type……… Linear\n"
+                + "  │       │   ├─Bound units…………………………………… nm\n"
+                + "  │       │   ├─Description…………………………………… Panchromatic\n"
+                + "  │       │   └─Name\n"
+                + "  │       │       └─Code…………………………………………… TestImage_B8.TIF\n"
                 + "  │       ├─Attribute (9 of 11)\n"
+                + "  │       │   ├─Max value………………………………………… 65535.0\n"
+                + "  │       │   ├─Min value………………………………………… 1.0\n"
                 + "  │       │   ├─Peak response……………………………… 1375.0\n"
-                + "  │       │   ├─Bound units…………………………………… nm\n"
-                + "  │       │   └─Description…………………………………… Cirrus\n"
+                + "  │       │   ├─Scale factor………………………………… 0.00242\n"
+                + "  │       │   ├─Offset………………………………………………… -12.1\n"
+                + "  │       │   ├─Transfer function type……… Linear\n"
+                + "  │       │   ├─Bound units…………………………………… nm\n"
+                + "  │       │   ├─Description…………………………………… Cirrus\n"
+                + "  │       │   └─Name\n"
+                + "  │       │       └─Code…………………………………………… TestImage_B9.TIF\n"
                 + "  │       ├─Attribute (10 of 11)\n"
+                + "  │       │   ├─Max value………………………………………… 65535.0\n"
+                + "  │       │   ├─Min value………………………………………… 1.0\n"
                 + "  │       │   ├─Peak response……………………………… 10800.0\n"
-                + "  │       │   ├─Bound units…………………………………… nm\n"
-                + "  │       │   └─Description…………………………………… Thermal Infrared Sensor (TIRS) 1\n"
+                + "  │       │   ├─Scale factor………………………………… 3.34E-4\n"
+                + "  │       │   ├─Offset………………………………………………… 0.1\n"
+                + "  │       │   ├─Transfer function type……… Linear\n"
+                + "  │       │   ├─Bound units…………………………………… nm\n"
+                + "  │       │   ├─Description…………………………………… Thermal Infrared Sensor (TIRS) 1\n"
+                + "  │       │   └─Name\n"
+                + "  │       │       └─Code…………………………………………… TestImage_B10.TIF\n"
                 + "  │       └─Attribute (11 of 11)\n"
+                + "  │           ├─Max value………………………………………… 65535.0\n"
+                + "  │           ├─Min value………………………………………… 1.0\n"
                 + "  │           ├─Peak response……………………………… 12000.0\n"
+                + "  │           ├─Scale factor………………………………… 3.34E-4\n"
+                + "  │           ├─Offset………………………………………………… 0.1\n"
+                + "  │           ├─Transfer function type……… Linear\n"
                 + "  │           ├─Bound units…………………………………… nm\n"
-                + "  │           └─Description…………………………………… Thermal Infrared Sensor (TIRS) 2\n"
+                + "  │           ├─Description…………………………………… Thermal Infrared Sensor (TIRS) 2\n"
+                + "  │           └─Name\n"
+                + "  │               └─Code…………………………………………… TestImage_B11.TIF\n"
                 + "  ├─Acquisition information\n"
                 + "  │   ├─Operation\n"
                 + "  │   │   ├─Status…………………………………………………………… Completed\n"

Modified: sis/branches/JDK8/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/MetadataBuilder.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/MetadataBuilder.java?rev=1759429&r1=1759428&r2=1759429&view=diff
==============================================================================
--- sis/branches/JDK8/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/MetadataBuilder.java [UTF-8] (original)
+++ sis/branches/JDK8/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/MetadataBuilder.java [UTF-8] Tue Sep  6 12:53:45 2016
@@ -20,6 +20,8 @@ import java.util.Date;
 import java.util.Locale;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
 import java.nio.charset.Charset;
 import org.opengis.util.InternationalString;
 import org.opengis.metadata.Identifier;
@@ -51,6 +53,7 @@ import org.apache.sis.metadata.iso.citat
 import org.apache.sis.metadata.iso.citation.DefaultIndividual;
 import org.apache.sis.metadata.iso.citation.DefaultOrganisation;
 import org.apache.sis.metadata.iso.constraint.DefaultLegalConstraints;
+import org.apache.sis.metadata.iso.identification.DefaultResolution;
 import org.apache.sis.metadata.iso.identification.DefaultDataIdentification;
 import org.apache.sis.metadata.iso.distribution.DefaultDistribution;
 import org.apache.sis.metadata.iso.distribution.DefaultFormat;
@@ -191,6 +194,14 @@ public class MetadataBuilder {
     private boolean electromagnetic;
 
     /**
+     * For using the same instance of {@code Double} when the value is the same.
+     * We use this map because the same values appear many time in a Landsat file.
+     *
+     * @see #parseDouble(String)
+     */
+    private final Map<Double,Double> sharedNumbers = new HashMap<>();
+
+    /**
      * Creates a new metadata reader.
      */
     public MetadataBuilder() {
@@ -493,6 +504,19 @@ public class MetadataBuilder {
     }
 
     /**
+     * Adds the given element in the collection if not already present.
+     * This method is used only for properties that are usually stored in {@code List} rather than {@code Set}
+     * and for which we do not keep a reference in this {@code MetadataBuilder} after the element has been added.
+     * This method is intended for adding elements that despite being modifiable, are not going to be modified by
+     * this {@code MetadataBuilder} class.
+     */
+    private static <E> void addIfNotPresent(final Collection<E> collection, final E element) {
+        if (!collection.contains(element)) {
+            collection.add(element);
+        }
+    }
+
+    /**
      * Adds a language used for documenting metadata.
      *
      * @param  language  a language used for documenting metadata.
@@ -578,7 +602,7 @@ public class MetadataBuilder {
         final DefaultGeographicBoundingBox bbox = new DefaultGeographicBoundingBox(
                     ordinates[index], ordinates[++index], ordinates[++index], ordinates[++index]);
         if (!bbox.isEmpty()) {
-            extent().getGeographicElements().add(bbox);
+            addIfNotPresent(extent().getGeographicElements(), bbox);
         }
     }
 
@@ -595,7 +619,7 @@ public class MetadataBuilder {
         if (startTime != null || endTime != null) {
             final DefaultTemporalExtent t = new DefaultTemporalExtent();
             t.setBounds(startTime, endTime);
-            extent().getTemporalElements().add(t);
+            addIfNotPresent(extent().getTemporalElements(), t);
         }
     }
 
@@ -608,7 +632,7 @@ public class MetadataBuilder {
      */
     public final void add(final Date date, final DateType type) {
         if (date != null) {
-            citation().getDates().add(new DefaultCitationDate(date, type));
+            addIfNotPresent(citation().getDates(), new DefaultCitationDate(date, type));
         }
     }
 
@@ -716,7 +740,7 @@ public class MetadataBuilder {
     public final void addCredits(final CharSequence credit) {
         final InternationalString i18n = trim(credit);
         if (i18n != null) {
-            identification().getCredits().add(i18n);
+            addIfNotPresent(identification().getCredits(), i18n);
         }
     }
 
@@ -728,8 +752,7 @@ public class MetadataBuilder {
      */
     public final void addIdentifier(String code) {
         if (code != null && !(code = code.trim()).isEmpty()) {
-            final DefaultIdentifier id = new DefaultIdentifier(code);
-            citation().getIdentifiers().add(id);
+            addIfNotPresent(citation().getIdentifiers(), new DefaultIdentifier(code));
         }
     }
 
@@ -978,7 +1001,7 @@ parse:      for (int i = 0; i < length;)
         if (identifier != null && !(identifier = identifier.trim()).isEmpty()) {
             final DefaultInstrument instrument = new DefaultInstrument();
             instrument.setIdentifier(new DefaultIdentifier(identifier));
-            platform().getInstruments().add(instrument);
+            addIfNotPresent(platform().getInstruments(), instrument);
         }
     }
 
@@ -998,7 +1021,71 @@ parse:      for (int i = 0; i < length;)
             op.setSignificantEvents(singleton(event));
             op.setType(OperationType.REAL);
             op.setStatus(Progress.COMPLETED);
-            acquisition().getOperations().add(op);
+            addIfNotPresent(acquisition().getOperations(), op);
+        }
+    }
+
+    /**
+     * Sets the area of the dataset obscured by clouds, expressed as a percentage of the spatial extent.
+     * This method does nothing if the given value is {@link Double#NaN}.
+     *
+     * <p>This method is available only if {@link #commitCoverageDescription(boolean)}
+     * has been invoked with the {@code electromagnetic} parameter set to {@code true}.</p>
+     *
+     * @param  value  the new cloud percentage.
+     * @throws IllegalArgumentException if the given value is out of range.
+     */
+    public final void setCloudCoverPercentage(final double value) {
+        if (!Double.isNaN(value)) {
+            ((DefaultImageDescription) coverageDescription()).setCloudCoverPercentage(shared(value));
+        }
+    }
+
+    /**
+     * Sets the illumination azimuth measured in degrees clockwise from true north at the time the image is taken.
+     * For images from a scanning device, refer to the centre pixel of the image.
+     * This method does nothing if the given value is {@link Double#NaN}.
+     *
+     * <p>This method is available only if {@link #commitCoverageDescription(boolean)}
+     * has been invoked with the {@code electromagnetic} parameter set to {@code true}.</p>
+     *
+     * @param  value  the new illumination azimuth angle, or {@code null}.
+     * @throws IllegalArgumentException if the given value is out of range.
+     */
+    public final void setIlluminationAzimuthAngle(final double value) {
+        if (!Double.isNaN(value)) {
+            ((DefaultImageDescription) coverageDescription()).setIlluminationAzimuthAngle(shared(value));
+        }
+    }
+
+    /**
+     * Sets the illumination elevation measured in degrees clockwise from the target plane
+     * at intersection of the optical line of sight with the Earth's surface.
+     * For images from a canning device, refer to the centre pixel of the image.
+     * This method does nothing if the given value is {@link Double#NaN}.
+     *
+     * <p>This method is available only if {@link #commitCoverageDescription(boolean)}
+     * has been invoked with the {@code electromagnetic} parameter set to {@code true}.</p>
+     *
+     * @param  value  the new illumination azimuth angle, or {@code null}.
+     * @throws IllegalArgumentException if the given value is out of range.
+     */
+    public final void setIlluminationElevationAngle(final double value) {
+        if (!Double.isNaN(value)) {
+            ((DefaultImageDescription) coverageDescription()).setIlluminationElevationAngle(shared(value));
+        }
+    }
+
+    /**
+     * Adds a linear resolution in metres.
+     *
+     * @param  distance  the resolution in metres, or {@code NaN} if none.
+     */
+    public final void addResolution(final double distance) {
+        if (!Double.isNaN(distance)) {
+            final DefaultResolution r = new DefaultResolution();
+            r.setDistance(shared(distance));
+            addIfNotPresent(identification().getSpatialResolutions(), r);
         }
     }
 
@@ -1010,7 +1097,7 @@ parse:      for (int i = 0; i < length;)
      */
     public final void addFormat(final CharSequence abbreviation) {
         if (abbreviation != null && abbreviation.length() != 0) {
-            identification().getResourceFormats().add(new DefaultFormat(abbreviation, null));
+            addIfNotPresent(identification().getResourceFormats(), new DefaultFormat(abbreviation, null));
         }
     }
 
@@ -1025,7 +1112,7 @@ parse:      for (int i = 0; i < length;)
             final DefaultSampleDimension sampleDimension = sampleDimension();
             final Double current = sampleDimension.getMinValue();
             if (current == null || value < current) {
-                sampleDimension.setMinValue(value);
+                sampleDimension.setMinValue(shared(value));
             }
         }
     }
@@ -1041,7 +1128,7 @@ parse:      for (int i = 0; i < length;)
             final DefaultSampleDimension sampleDimension = sampleDimension();
             final Double current = sampleDimension.getMaxValue();
             if (current == null || value > current) {
-                sampleDimension.setMaxValue(value);
+                sampleDimension.setMaxValue(shared(value));
             }
         }
     }
@@ -1060,57 +1147,6 @@ parse:      for (int i = 0; i < length;)
     }
 
     /**
-     * Sets the area of the dataset obscured by clouds, expressed as a percentage of the spatial extent.
-     * This method does nothing if the given value is {@link Double#NaN}.
-     *
-     * <p>This method is available only if {@link #commitCoverageDescription(boolean)}
-     * has been invoked with the {@code electromagnetic} parameter set to {@code true}.</p>
-     *
-     * @param  value  the new cloud percentage.
-     * @throws IllegalArgumentException if the given value is out of range.
-     */
-    public final void setCloudCoverPercentage(final double value) {
-        if (!Double.isNaN(value)) {
-            ((DefaultImageDescription) coverageDescription()).setCloudCoverPercentage(value);
-        }
-    }
-
-    /**
-     * Sets the illumination azimuth measured in degrees clockwise from true north at the time the image is taken.
-     * For images from a scanning device, refer to the centre pixel of the image.
-     * This method does nothing if the given value is {@link Double#NaN}.
-     *
-     * <p>This method is available only if {@link #commitCoverageDescription(boolean)}
-     * has been invoked with the {@code electromagnetic} parameter set to {@code true}.</p>
-     *
-     * @param  value  the new illumination azimuth angle, or {@code null}.
-     * @throws IllegalArgumentException if the given value is out of range.
-     */
-    public final void setIlluminationAzimuthAngle(final double value) {
-        if (!Double.isNaN(value)) {
-            ((DefaultImageDescription) coverageDescription()).setIlluminationAzimuthAngle(value);
-        }
-    }
-
-    /**
-     * Sets the illumination elevation measured in degrees clockwise from the target plane
-     * at intersection of the optical line of sight with the Earth's surface.
-     * For images from a canning device, refer to the centre pixel of the image.
-     * This method does nothing if the given value is {@link Double#NaN}.
-     *
-     * <p>This method is available only if {@link #commitCoverageDescription(boolean)}
-     * has been invoked with the {@code electromagnetic} parameter set to {@code true}.</p>
-     *
-     * @param  value  the new illumination azimuth angle, or {@code null}.
-     * @throws IllegalArgumentException if the given value is out of range.
-     */
-    public final void setIlluminationElevationAngle(final double value) {
-        if (!Double.isNaN(value)) {
-            ((DefaultImageDescription) coverageDescription()).setIlluminationElevationAngle(value);
-        }
-    }
-
-    /**
      * Returns the metadata (optionally as an unmodifiable object), or {@code null} if none.
      * If {@code freeze} is {@code true}, then the returned metadata instance can not be modified.
      *
@@ -1130,4 +1166,25 @@ parse:      for (int i = 0; i < length;)
         }
         return md;
     }
+
+    /**
+     * Parses the given {@code double} value, returning a shared instance if possible.
+     * This is a helper method for callers who want to set themselves some additional
+     * metadata values on the instance returned by {@link #build(boolean)}.
+     *
+     * @param   value  the string value to parse.
+     * @return  the parsed value.
+     * @throws  NumberFormatException if the given value can not be parsed.
+     */
+    public final Double parseDouble(final String value) throws NumberFormatException {
+        return shared(Double.valueOf(value));
+    }
+
+    /**
+     * Returns a shared instance of the given value.
+     */
+    private Double shared(final Double value) {
+        final Double existing = sharedNumbers.putIfAbsent(value, value);
+        return (existing != null) ? existing : value;
+    }
 }



Mime
View raw message