| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.sis.storage.earthobservation; |
| |
| import java.io.BufferedReader; |
| import java.io.IOException; |
| import java.util.Arrays; |
| import java.util.Date; |
| import java.util.Locale; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| import java.io.LineNumberReader; |
| import java.time.LocalDate; |
| import java.time.OffsetDateTime; |
| import java.time.OffsetTime; |
| import java.time.DateTimeException; |
| import java.time.temporal.Temporal; |
| |
| import org.opengis.metadata.Metadata; |
| import org.opengis.metadata.citation.DateType; |
| import org.opengis.metadata.identification.TopicCategory; |
| import org.opengis.metadata.spatial.DimensionNameType; |
| import org.opengis.metadata.content.CoverageContentType; |
| import org.opengis.metadata.content.TransferFunctionType; |
| import org.opengis.metadata.maintenance.ScopeCode; |
| import org.opengis.parameter.ParameterValueGroup; |
| import org.opengis.referencing.crs.ProjectedCRS; |
| import org.opengis.util.NoSuchIdentifierException; |
| import org.opengis.util.FactoryException; |
| |
| import org.apache.sis.measure.Units; |
| import org.apache.sis.metadata.iso.DefaultMetadata; |
| import org.apache.sis.metadata.iso.DefaultIdentifier; |
| import org.apache.sis.metadata.iso.content.DefaultAttributeGroup; |
| import org.apache.sis.metadata.iso.content.DefaultBand; |
| import org.apache.sis.metadata.iso.content.DefaultCoverageDescription; |
| import org.apache.sis.metadata.sql.MetadataStoreException; |
| import org.apache.sis.referencing.CRS; |
| import org.apache.sis.referencing.CommonCRS; |
| import org.apache.sis.storage.DataStoreException; |
| import org.apache.sis.storage.DataStoreReferencingException; |
| import org.apache.sis.storage.event.StoreListeners; |
| import org.apache.sis.util.Characters; |
| import org.apache.sis.util.CharSequences; |
| import org.apache.sis.util.resources.Errors; |
| import org.apache.sis.util.resources.Vocabulary; |
| import org.apache.sis.util.iso.SimpleInternationalString; |
| import org.apache.sis.internal.referencing.GeodeticObjectBuilder; |
| import org.apache.sis.internal.referencing.ReferencingFactoryContainer; |
| import org.apache.sis.internal.referencing.provider.PolarStereographicB; |
| import org.apache.sis.internal.referencing.provider.TransverseMercator; |
| import org.apache.sis.internal.storage.MetadataBuilder; |
| import org.apache.sis.internal.util.StandardDateFormat; |
| import org.apache.sis.internal.util.Constants; |
| import org.apache.sis.internal.util.Strings; |
| |
| import static org.apache.sis.internal.util.CollectionsExt.singletonOrNull; |
| |
| |
| /** |
| * Parses Landsat metadata as {@linkplain DefaultMetadata ISO 19115 Metadata} object. |
| * This class reads the content of a given {@link BufferedReader} from buffer position |
| * until the first occurrence of the {@code END} keyword. Lines beginning with the |
| * {@code #} character (ignoring spaces) are treated as comment lines and ignored. |
| * |
| * <p>This class will parse properties found in the Landsat metadata file, |
| * except {@code GROUP} and {@code END_GROUP}. Example: |
| * |
| * {@preformat text |
| * DATE_ACQUIRED = 2014-03-12 |
| * SCENE_CENTER_TIME = 03:02:01.5339408Z |
| * CORNER_UL_LAT_PRODUCT = 12.61111 |
| * CORNER_UL_LON_PRODUCT = 108.33624 |
| * CORNER_UR_LAT_PRODUCT = 12.62381 |
| * CORNER_UR_LON_PRODUCT = 110.44017 |
| * } |
| * |
| * <p><b>NOTE FOR MAINTAINER:</b> if the work performed by this class is modified, consider updating |
| * <a href="./doc-files/LandsatMetadata.html">./doc-files/LandsatMetadata.html</a> accordingly.</p> |
| * |
| * @author Thi Phuong Hao Nguyen (VNSC) |
| * @author Rémi Maréchal (Geomatys) |
| * @author Martin Desruisseaux (Geomatys) |
| * @version 1.0 |
| * @since 0.8 |
| * @module |
| */ |
| final class LandsatReader extends MetadataBuilder { |
| /** |
| * Names of Landsat bands. |
| * |
| * @todo Those names and the wavelength could be moved to the {@code SpatialMetadata} database, |
| * as described in <a href="https://issues.apache.org/jira/browse/SIS-338">SIS-338</a>. |
| * It would make easier to enrich the metadata with more information. |
| * |
| * @see #bands |
| * @see #band(String, int) |
| */ |
| 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(String, 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 |
| * “Image courtesy of the U.S. Geological Survey”. |
| */ |
| static final Pattern CREDIT = Pattern.compile("\\bcourtesy\\h+of\\h+(the)?\\b\\s*", Pattern.CASE_INSENSITIVE); |
| |
| /** |
| * Number of spatial dimensions. This is the number of coordinate values to be stored |
| * in the {@link #gridSizes} and {@link #corners} arrays for each tuple. |
| */ |
| static final int DIM = 2; |
| |
| /** |
| * 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"; |
| |
| /** |
| * A bit mask of the group in which to classify a given band. |
| * There is three groups: panchromatic, reflective or thermal bands: |
| * |
| * <ul> |
| * <li><b>Panchromatic:</b> band 8.</li> |
| * <li><b>Reflective:</b> bands 1, 2, 3, 4, 5, 6, 7, 9.</li> |
| * <li><b>Thermal:</b> bands 10, 11.</li> |
| * </ul> |
| * |
| * For a band numbered from 1 to 11 inclusive, the group is computed by |
| * (constants 2 and 3 in that formula depends on the {@link #NUM_GROUPS} value): |
| * |
| * {@preformat java |
| * group = (BAND_GROUPS >>> 2*(band - 1)) & 3; |
| * } |
| * |
| * The result is one of the {@link #PANCHROMATIC}, {@link #REFLECTIVE} or {@link #THERMAL} constant values |
| * divided by {@value #DIM}. |
| */ |
| static final int BAND_GROUPS = 2692437; // Value computed by LandsatReaderTest.verifyBandGroupsMask() |
| |
| /** |
| * Maximum number of band groups that a metadata may contains. |
| * See {@link #BAND_GROUPS} javadoc for the list of groups. |
| */ |
| private static final int NUM_GROUPS = 3; |
| |
| /** |
| * Index of panchromatic, reflective or thermal groups in the {@link #gridSizes} array. |
| * The image size is each group is given by {@value #DIM} integers: the width and the height. |
| */ |
| static final int PANCHROMATIC = 0*DIM, |
| REFLECTIVE = 1*DIM, |
| THERMAL = 2*DIM; |
| |
| /** |
| * Index of projected and geographic coordinates in the {@link #corners} array. |
| * Each kind of coordinates are stored as 4 corners of {@value #DIM} coordinate values. |
| */ |
| private static final int PROJECTED = 0*DIM, |
| GEOGRAPHIC = 4*DIM; |
| |
| /** |
| * The keyword for end of metadata file. |
| */ |
| private static final String END = "END"; |
| |
| /** |
| * An identifier of the file being read, or {@code null} if unknown. |
| * This is used mostly for formatting error messages. |
| * |
| * @see #getFilename() |
| */ |
| private String filename; |
| |
| /** |
| * Where to send the warnings. |
| */ |
| private final StoreListeners listeners; |
| |
| /** |
| * Group in process of being parsed, or {@code null} if none. |
| */ |
| private String group; |
| |
| /** |
| * The acquisition time, or {@code null} if not yet known. This needs to be parsed in two steps: |
| * first by parsing the {@code "DATE_ACQUIRED"} attribute, then {@code "SCENE_CENTER_TIME"}. |
| * |
| * @see #flushSceneTime() |
| */ |
| private Temporal sceneTime; |
| |
| /** |
| * Projected and geographic coordinate values, stocked temporarily before to be saved in the extent. |
| * Values are in (<var>x</var>,<var>y</var>) or (<var>lon</var>,<var>lat</var>) order. |
| * The first 8 values are the projected ones. The next 8 values are the geographic ones. |
| * Corner order is UL, UR, LL, LR. |
| */ |
| private final double[] corners; |
| |
| /** |
| * Image width and hight in pixels, as unsigned integers. 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(String, int) |
| */ |
| private final DefaultBand[] bands; |
| |
| /** |
| * The enumeration for the {@code "DATUM"} element, to be used for creating the Coordinate Reference System. |
| */ |
| private CommonCRS datum; |
| |
| /** |
| * The Universal Transverse Mercator (UTM) zone as a number from 1 to 60 inclusive, or 0 if the zone has not |
| * yet been determined. If the parser determined that the projection is Polar Stereographic, then this field |
| * is set to -1. |
| */ |
| private short utmZone; |
| |
| /** |
| * The map projection parameters. This is used only for the polar stereographic case. |
| */ |
| private ParameterValueGroup projection; |
| |
| /** |
| * The referencing objects factories. |
| */ |
| private final ReferencingFactoryContainer factories; |
| |
| /** |
| * Creates a new metadata parser. |
| * |
| * @param filename an identifier of the file being read, or {@code null} if unknown. |
| * @param listeners where to sent warnings that may occur during the parsing process. |
| */ |
| LandsatReader(final String filename, final StoreListeners listeners) { |
| this.filename = filename; |
| this.listeners = listeners; |
| this.factories = new ReferencingFactoryContainer(); |
| this.bands = new DefaultBand[BAND_NAMES.length]; |
| this.gridSizes = new int[NUM_GROUPS * DIM]; |
| this.corners = new double[GEOGRAPHIC + (4*DIM)]; // GEOGRAPHIC is the last group of corners to store. |
| Arrays.fill(corners, Double.NaN); |
| } |
| |
| /** |
| * Parses the metadata from the given characters reader. |
| * The parsing stop after the first {@code "END"} keyword. |
| * See class javadoc for more information on the expected format. |
| * |
| * @param reader a reader opened on the Landsat file. |
| * It is caller's responsibility to close this reader. |
| * @throws IOException if an I/O error occurred while reading the given stream. |
| * @throws DataStoreException if the content is not a Landsat file. |
| */ |
| void read(final BufferedReader reader) throws IOException, DataStoreException { |
| newCoverage(true); // Starts the description of a new image. |
| String line; |
| while ((line = reader.readLine()) != null) { |
| int end = CharSequences.skipTrailingWhitespaces(line, 0, line.length()); |
| int start = CharSequences.skipLeadingWhitespaces(line, 0, end); |
| if (start < end && line.charAt(start) != '#') { |
| /* |
| * Separate the line into its key and value. For example in CORNER_UL_LAT_PRODUCT = 12.61111, |
| * the key will be CORNER_UL_LAT_PRODUCT and the value will be 12.61111. |
| */ |
| final int separator = line.indexOf('=', start); |
| if (separator < 0) { |
| /* |
| * 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 (end - start != END.length() || !line.regionMatches(true, start, END, 0, END.length())) { |
| throw new DataStoreException(errors().getString(Errors.Keys.NotAKeyValuePair_1, line)); |
| } |
| return; |
| } |
| /* |
| * 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); |
| } |
| } |
| } |
| listeners.warning(errors().getString(Errors.Keys.UnexpectedEndOfFile_1, getFilename())); |
| } |
| |
| /** |
| * Parses the given string as a {@code double} value, returning a shared instance if possible. |
| * |
| * @param value the string value to parse. |
| * @return the parsed value. |
| * @throws NumberFormatException if the given value can not be parsed. |
| */ |
| private Double parseDouble(final String value) throws NumberFormatException { |
| return shared(Double.valueOf(value)); |
| } |
| |
| /** |
| * 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 coordinate 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}, |
| * +1 if 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.parseUnsignedInt(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. |
| * @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 int band, String value) |
| throws IllegalArgumentException, DateTimeException, DataStoreException |
| { |
| switch (key) { |
| case "GROUP": { |
| group = value; |
| break; |
| } |
| case "END_GROUP": { |
| group = null; |
| break; |
| } |
| |
| //// |
| //// GROUP = METADATA_FILE_INFO |
| //// |
| |
| /* |
| * Origin of the product. |
| * Value is "Image courtesy of the U.S. Geological Survey". |
| */ |
| case "ORIGIN": { |
| final Matcher m = CREDIT.matcher(value); |
| if (m.find()) { |
| newParty(MetadataBuilder.PartyType.ORGANISATION); |
| addAuthor(value.substring(m.end())); |
| } |
| addCredits(value); |
| break; |
| } |
| /* |
| * Product Request ID. NNNNNNNNNNNNN_UUUUU, where NNNNNNNNNNNNN = 13-digit Tracking, |
| * Routing, and Metrics (TRAM) order number and UUUUU = 5-digit TRAM unit number. |
| * Example: "0501403126384_00011" |
| */ |
| case "REQUEST_ID": { |
| addAcquisitionRequirement(null, value); |
| break; |
| } |
| /* |
| * The unique Landsat scene identifier. |
| * Format is {@code Ls8ppprrrYYYYDDDGGGVV}. |
| * Example: "LC81230522014071LGN00". |
| */ |
| case "LANDSAT_SCENE_ID": { |
| addTitleOrIdentifier(value, MetadataBuilder.Scope.ALL); |
| break; |
| } |
| /* |
| * The date when the metadata file for the L1G product set was created. |
| * The date is based on Universal Time Coordinated (UTC). |
| * Date format is {@code YYYY-MM-DDTHH:MM:SSZ}. |
| * Example: "2014-03-12T06:06:35Z". |
| */ |
| case "FILE_DATE": { |
| addCitationDate(StandardDateFormat.toDate(OffsetDateTime.parse(value)), |
| DateType.CREATION, MetadataBuilder.Scope.ALL); |
| break; |
| } |
| /* |
| * The Ground Station that received the data. Grounds station identifiers are specified in LSDS-547. |
| * Example: "LGN" = Landsat Ground Network. |
| */ |
| // TODO case "STATION_ID": |
| /* |
| * The processing software version that created the product. Can be "IAS_X.Y.Z" or "LPGS_X.Y.Z" |
| * where X, Y and Z are major, minor and patch version numbers. |
| * Example: "LPGS_2.3.0". |
| */ |
| // TODO case "PROCESSING_SOFTWARE_VERSION": |
| |
| //// |
| //// GROUP = PRODUCT_METADATA |
| //// |
| |
| /* |
| * The identifier to inform the user of the product type. |
| * Value can be "L1T" or "L1GT". |
| */ |
| case "DATA_TYPE": { |
| setProcessingLevelCode("Landsat", value); |
| break; |
| } |
| /* |
| * Indicates the source of the DEM used in the correction process. |
| * Value can be "GLS2000", "RAMP" or "GTOPO30". |
| */ |
| case "ELEVATION_SOURCE": { |
| addSource(value, ScopeCode.MODEL, Vocabulary.formatInternational(Vocabulary.Keys.DigitalElevationModel)); |
| break; |
| } |
| /* |
| * The output format of the image. |
| * Value is "GEOTIFF". |
| */ |
| case "OUTPUT_FORMAT": { |
| if (Constants.GEOTIFF.equalsIgnoreCase(value)) try { |
| value = Constants.GEOTIFF; // Because 'metadata.setFormat(…)' is case-sensitive. |
| setFormat(value); |
| break; |
| } catch (MetadataStoreException e) { |
| warning(key, null, e); |
| } |
| addFormatName(value); |
| break; |
| } |
| /* |
| * Spacecraft from which the data were captured. |
| * Example: "LANDSAT_8". |
| */ |
| case "SPACECRAFT_ID": { |
| addPlatform(null, value); |
| break; |
| } |
| /* |
| * Sensor(s) used to capture this scene. |
| * Example: "OLI", "TIRS" or "OLI_TIRS". |
| */ |
| case "SENSOR_ID": { |
| addInstrument(null, value); |
| break; |
| } |
| /* |
| * The date the image was acquired. |
| * Date format is {@code YYYY-MM-DD}. |
| * Example: "2014-03-12". |
| */ |
| case "DATE_ACQUIRED": { |
| final LocalDate date = LocalDate.parse(value); |
| if (sceneTime instanceof OffsetTime) { |
| sceneTime = date.atTime((OffsetTime) sceneTime); |
| } else if (!date.equals(sceneTime)) { |
| flushSceneTime(); |
| sceneTime = date; |
| } |
| break; |
| } |
| /* |
| * Scene center time of the date the image was acquired. |
| * Time format is {@code HH:MI:SS.SSSSSSSZ}. |
| * Example: "03:02:01.5339408Z". |
| */ |
| case "SCENE_CENTER_TIME": { |
| final OffsetTime time = OffsetTime.parse(value); |
| if (sceneTime instanceof LocalDate) { |
| sceneTime = ((LocalDate) sceneTime).atTime(time); |
| } else { |
| sceneTime = time; |
| } |
| break; |
| } |
| /* |
| * The longitude and latitude values for the upper-left (UL), upper-right (UR), lower-left (LL) |
| * and lower-right (LR) corners of the product, measured at the center of the pixel. |
| * Positive longitude value indicates east longitude; negative value indicates west longitude. |
| * Positive latitude value indicates north latitude; negative value indicates south latitude. |
| * Units are in degrees. |
| */ |
| 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": { |
| addResolution(Double.parseDouble(value)); |
| break; |
| } |
| /* |
| * The file name of the TIFF image that contains the pixel values for a band. |
| * This parameter is only present if the band is included in the product. |
| */ |
| 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". |
| */ |
| case "METADATA_FILE_NAME": { |
| if (filename == null) { |
| filename = value; |
| } |
| break; |
| } |
| |
| //// |
| //// GROUP = IMAGE_ATTRIBUTES |
| //// |
| |
| /* |
| * The overall cloud coverage (percent) of the WRS-2 scene as a value between 0 and 100 inclusive. |
| * -1 indicates that the score was not calculated. |
| */ |
| case "CLOUD_COVER": { |
| final double v = Double.parseDouble(value); |
| if (v >= 0) setCloudCoverPercentage(v); |
| break; |
| } |
| /* |
| * The Sun azimuth angle in degrees for the image center location at the image center acquisition time. |
| * Values are from -180 to 180 degrees inclusive. |
| * A positive value indicates angles to the east or clockwise from the north. |
| * A negative value indicates angles to the west or counterclockwise from the north. |
| */ |
| case "SUN_AZIMUTH": { |
| setIlluminationAzimuthAngle(Double.parseDouble(value)); |
| break; |
| } |
| /* |
| * The Sun elevation angle in degrees for the image center location at the image center acquisition time. |
| * Values are from -90 to 90 degrees inclusive. |
| * A positive value indicates a daytime scene. A negative value indicates a nighttime scene. |
| * Note: for reflectance calculation, the sun zenith angle is needed, which is 90 - sun elevation angle. |
| */ |
| case "SUN_ELEVATION": { |
| 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 = 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 = 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; |
| } |
| |
| //// |
| //// GROUP = PROJECTION_PARAMETERS |
| //// |
| |
| /* |
| * The map projection used in creating the image. |
| * Universal Transverse Mercator (UTM) or Polar Stereographic (PS). |
| */ |
| case "MAP_PROJECTION": { |
| if ("UTM".equalsIgnoreCase(value)) { |
| projection = null; |
| } else if ("PS".equalsIgnoreCase(value)) try { |
| projection = factories.getMathTransformFactory() |
| .getDefaultParameters(Constants.EPSG + ':' + PolarStereographicB.IDENTIFIER); |
| utmZone = -1; |
| } catch (NoSuchIdentifierException e) { |
| // Should never happen with Apache SIS implementation of MathTransformFactory. |
| throw new DataStoreReferencingException(e); |
| } |
| break; |
| } |
| /* |
| * The datum used in creating the image. This is usually "WGS84". |
| * We ignore the "ELLIPSOID" attribute because it is implied by the datum. |
| */ |
| case "DATUM": { |
| datum = CommonCRS.valueOf(Strings.toUpperCase(value, Characters.Filter.LETTERS_AND_DIGITS)); |
| break; |
| } |
| /* |
| * The value used to indicate the zone number. This parameter is only included for the UTM projection. |
| * If this parameter is defined more than once (which should be illegal), only the first occurrence is |
| * retained. If the projection is polar stereographic, the parameter is ignored. |
| */ |
| case "UTM_ZONE": { |
| if (utmZone == 0) { |
| utmZone = Short.parseShort(value); |
| } |
| break; |
| } |
| /* |
| * Polar Stereographic projection parameters. Most parameters do not vary, except the latitude of |
| * true scale which is -71 for scenes over Antarctica and 71 for off-nadir scenes at the North Pole. |
| * If the datum is WGS84, then this is equivalent to EPSG:3031 and EPSG:3995 respectively. |
| */ |
| case "VERTICAL_LON_FROM_POLE": setProjectionParameter(key, Constants.CENTRAL_MERIDIAN, value, false); break; |
| case "TRUE_SCALE_LAT": setProjectionParameter(key, Constants.STANDARD_PARALLEL_1, value, false); break; |
| case "FALSE_EASTING": setProjectionParameter(key, Constants.FALSE_EASTING, value, true); break; |
| case "FALSE_NORTHING": setProjectionParameter(key, Constants.FALSE_NORTHING, value, true); 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 = 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)); |
| 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(Units.NANOMETRE); |
| bands[index] = band; |
| } |
| return band; |
| } |
| |
| /** |
| * Sets a map projection parameter. The parameter is ignored if the projection has not been set. |
| * |
| * @param key the Landsat key, for formatting error message if needed. |
| * @param name the projection parameter name. |
| * @param value the parameter value. |
| * @param isLinear {@code true} for value in metres, or {@code false} for value in degrees. |
| */ |
| private void setProjectionParameter(final String key, final String name, final String value, final boolean isLinear) { |
| if (projection != null) { |
| projection.parameter(name).setValue(Double.parseDouble(value), isLinear ? Units.METRE : Units.DEGREE); |
| } else { |
| listeners.warning(errors().getString(Errors.Keys.UnexpectedProperty_2, filename, key)); |
| } |
| } |
| |
| /** |
| * Writes the value of {@link #sceneTime} into the metadata object as a temporal extent. |
| * |
| * @throws DateTimeException if {@link #sceneTime} is an instance of {@link OffsetTime}. This may |
| * happen if {@code SCENE_CENTER_TIME} attribute was found without {@code DATE_ACQUIRED}. |
| */ |
| private void flushSceneTime() { |
| final Temporal st = sceneTime; |
| if (st != null) { |
| sceneTime = null; // Clear now in case an exception it thrown below. |
| final Date t = StandardDateFormat.toDate(st); |
| addAcquisitionTime(t); |
| try { |
| addTemporalExtent(t, t); |
| } catch (UnsupportedOperationException e) { |
| // May happen if the temporal module (which is optional) is not on the classpath. |
| warning(null, null, e); |
| } |
| } |
| } |
| |
| /** |
| * Computes the bounding box for the 8 {@link #corners} values starting at the given index. |
| * Valid indices are 0 for the projected envelope or 8 for the geographic bounding box. |
| * Result is stored in the 4 values starting the given {@code base} index. |
| * |
| * @return {@code true} of success, or {@code false} if there is no bounding box. |
| */ |
| private boolean toBoundingBox(int base) { |
| double xmin = Double.POSITIVE_INFINITY; |
| double ymin = Double.POSITIVE_INFINITY; |
| double xmax = Double.NEGATIVE_INFINITY; |
| double ymax = Double.NEGATIVE_INFINITY; |
| for (int i = base + (4*DIM); --i >= base;) { |
| double v = corners[i]; |
| if (v < ymin) ymin = v; |
| if (v > ymax) ymax = v; |
| v = corners[--i]; |
| if (v < xmin) xmin = v; |
| if (v > xmax) xmax = v; |
| } |
| if (xmin < xmax && ymin < ymax) { |
| corners[ base] = xmin; |
| corners[++base] = xmax; |
| corners[++base] = ymin; |
| corners[++base] = ymax; |
| return true; |
| } |
| return false; |
| } |
| |
| /** |
| * Returns the metadata about the resources described in the Landsat file. |
| * The {@link #read(BufferedReader)} method must be invoked at least once before. |
| * |
| * @throws FactoryException if an error occurred while creating the Coordinate Reference System. |
| */ |
| final Metadata getMetadata() throws FactoryException { |
| addLanguage(Locale.ENGLISH, MetadataBuilder.Scope.METADATA); |
| addResourceScope(ScopeCode.COVERAGE, null); |
| addTopicCategory(TopicCategory.GEOSCIENTIFIC_INFORMATION); |
| try { |
| flushSceneTime(); |
| } catch (DateTimeException e) { |
| // May happen if the SCENE_CENTER_TIME attribute was found without DATE_ACQUIRED. |
| warning(null, null, e); |
| } |
| /* |
| * Create the Coordinate Reference System. We normally have only one of UTM or Polar Stereographic, |
| * but this block is nevertheless capable to take both (such metadata are likely to be invalid, but |
| * we can not guess which one of the two CRS is correct). |
| */ |
| if (datum != null) { |
| if (utmZone > 0) { |
| addReferenceSystem(datum.universal(1, TransverseMercator.Zoner.UTM.centralMeridian(utmZone))); |
| } |
| if (projection != null) { |
| final double sp = projection.parameter(Constants.STANDARD_PARALLEL_1).doubleValue(); |
| ProjectedCRS crs = (ProjectedCRS) CRS.forCode(Constants.EPSG + ":" + |
| (sp >= 0 ? Constants.EPSG_ARCTIC_POLAR_STEREOGRAPHIC // Standard parallel = 71°N |
| : Constants.EPSG_ANTARCTIC_POLAR_STEREOGRAPHIC)); // Standard parallel = 71°S |
| if (datum != CommonCRS.WGS84 || Math.abs(sp) != 71 |
| || projection.parameter(Constants.FALSE_EASTING) .doubleValue() != 0 |
| || projection.parameter(Constants.FALSE_NORTHING) .doubleValue() != 0 |
| || projection.parameter(Constants.CENTRAL_MERIDIAN).doubleValue() != 0) |
| { |
| crs = new GeodeticObjectBuilder(listeners.getLocale()) |
| .addName("Polar stereographic").setConversion(projection) |
| .createProjectedCRS(datum.geographic(), crs.getCoordinateSystem()); |
| } |
| addReferenceSystem(crs); |
| } |
| } |
| /* |
| * Set information about envelope (or geographic area) and grid size. |
| */ |
| if (toBoundingBox(GEOGRAPHIC)) { |
| addExtent(corners, GEOGRAPHIC); |
| } |
| for (int i = 0; i < gridSizes.length; i += DIM) { |
| final int width = gridSizes[i ]; |
| final int height = gridSizes[i+1]; |
| if ((width | height) != 0) { |
| newGridRepresentation(MetadataBuilder.GridType.GEORECTIFIED); |
| setAxisName(0, DimensionNameType.SAMPLE); |
| setAxisName(1, DimensionNameType.LINE); |
| setAxisSize(0, Integer.toUnsignedLong(width)); |
| setAxisSize(1, Integer.toUnsignedLong(height)); |
| } |
| } |
| /* |
| * At this point we are done configuring he metadata builder. Creates the ISO 19115 metadata instance, |
| * then continue adding some more specific metadata elements by ourself. For example information about |
| * bands are splitted in 3 different AttributeGroups based on their grid size. |
| */ |
| setISOStandards(true); |
| final DefaultMetadata result = build(false); |
| if (result != null) { |
| /* |
| * Set information about all non-null bands. The bands are categorized in three groups: |
| * PANCHROMATIC, REFLECTIVE and THERMAL. The group in which each band belong is encoded |
| * in the BAND_GROUPS bitmask. |
| */ |
| final DefaultCoverageDescription content = (DefaultCoverageDescription) singletonOrNull(result.getContentInfo()); |
| if (content != null) { |
| final DefaultAttributeGroup[] groups = new DefaultAttributeGroup[NUM_GROUPS]; |
| for (int i=0; i < bands.length; i++) { |
| final DefaultBand band = bands[i]; |
| if (band != null) { |
| final int gi = (BAND_GROUPS >>> 2*i) & 3; |
| DefaultAttributeGroup group = groups[gi]; |
| if (group == null) { |
| group = new DefaultAttributeGroup(CoverageContentType.PHYSICAL_MEASUREMENT, null); |
| content.getAttributeGroups().add(group); |
| groups[gi] = group; |
| } |
| group.getAttributes().add(band); |
| } |
| } |
| } |
| result.transitionTo(DefaultMetadata.State.FINAL); |
| } |
| return result; |
| } |
| |
| /** |
| * Returns the filename to show in error messages, or a localized "unnamed" word if none. |
| */ |
| private String getFilename() { |
| return (filename != null) ? filename : Vocabulary.getResources(listeners.getLocale()).getString(Vocabulary.Keys.Unnamed); |
| } |
| |
| /** |
| * Prepends the group name before the given key, if a group name exists. |
| * This is used only for formatting warning messages. |
| */ |
| private String toLongName(String key) { |
| if (group != null) { |
| key = group + ':' + key; |
| } |
| return key; |
| } |
| |
| /** |
| * 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. |
| */ |
| 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); |
| } |
| |
| /** |
| * Returns the resources to use for formatting error messages. |
| */ |
| private Errors errors() { |
| return Errors.getResources(listeners.getLocale()); |
| } |
| } |