/* * Geotoolkit - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2016, Geomatys * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. */ package org.geotoolkit.coverage.landsat; import java.awt.geom.AffineTransform; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Set; import java.util.Locale; import java.util.logging.Level; import java.util.logging.Logger; import java.util.UUID; import java.text.DecimalFormat; import java.text.NumberFormat; import java.text.ParseException; import org.opengis.coverage.Coverage; import org.opengis.coverage.grid.GridEnvelope; import org.opengis.geometry.Envelope; import org.opengis.metadata.citation.DateType; import org.opengis.metadata.content.AttributeGroup; import org.opengis.metadata.content.Band; import org.opengis.metadata.content.ImageDescription; import org.opengis.metadata.content.TransferFunctionType; import org.opengis.metadata.identification.Resolution; import org.opengis.metadata.lineage.ProcessStep; import org.opengis.parameter.ParameterValueGroup; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.crs.TemporalCRS; import org.opengis.referencing.cs.CoordinateSystem; import org.opengis.referencing.IdentifiedObject; import org.opengis.referencing.operation.CoordinateOperationFactory; import org.opengis.referencing.operation.MathTransform; import org.opengis.referencing.operation.OperationMethod; import org.opengis.util.FactoryException; import org.apache.sis.geometry.GeneralEnvelope; import org.apache.sis.internal.referencing.GeodeticObjectBuilder; import org.apache.sis.internal.referencing.j2d.AffineTransform2D; import org.apache.sis.internal.system.DefaultFactories; import org.apache.sis.internal.util.Constants; import org.apache.sis.metadata.iso.DefaultIdentifier; import org.apache.sis.metadata.iso.DefaultMetadata; import org.apache.sis.metadata.iso.acquisition.DefaultAcquisitionInformation; import org.apache.sis.metadata.iso.acquisition.DefaultInstrument; import org.apache.sis.metadata.iso.acquisition.DefaultPlatform; import org.apache.sis.metadata.iso.citation.DefaultCitation; import org.apache.sis.metadata.iso.citation.DefaultCitationDate; import org.apache.sis.metadata.iso.content.DefaultAttributeGroup; import org.apache.sis.metadata.iso.content.DefaultBand; import org.apache.sis.metadata.iso.content.DefaultImageDescription; import org.apache.sis.metadata.iso.distribution.DefaultDistribution; import org.apache.sis.metadata.iso.distribution.DefaultFormat; import org.apache.sis.metadata.iso.extent.DefaultExtent; import org.apache.sis.metadata.iso.extent.DefaultGeographicBoundingBox; import org.apache.sis.metadata.iso.extent.DefaultTemporalExtent; import org.apache.sis.metadata.iso.identification.DefaultDataIdentification; import org.apache.sis.metadata.iso.identification.DefaultResolution; import org.apache.sis.metadata.iso.lineage.DefaultLineage; import org.apache.sis.metadata.iso.lineage.DefaultProcessStep; import org.apache.sis.referencing.CRS; import org.apache.sis.referencing.CommonCRS; import org.apache.sis.referencing.NamedIdentifier; import org.apache.sis.referencing.crs.DefaultProjectedCRS; import org.apache.sis.referencing.operation.DefaultConversion; import org.apache.sis.referencing.operation.matrix.AffineTransforms2D; import org.apache.sis.referencing.operation.transform.LinearTransform; import org.apache.sis.referencing.operation.transform.MathTransforms; import org.apache.sis.util.ArgumentChecks; import org.apache.sis.util.iso.DefaultInternationalString; import org.apache.sis.util.logging.Logging; import org.geotoolkit.coverage.grid.GeneralGridEnvelope; import org.geotoolkit.metadata.Citations; import org.geotoolkit.referencing.cs.PredefinedCS; import org.geotoolkit.referencing.operation.builder.LocalizationGrid; import org.geotoolkit.temporal.object.DefaultInstant; import org.geotoolkit.temporal.object.DefaultPeriod; import org.geotoolkit.temporal.util.TimeParser; import static org.geotoolkit.coverage.landsat.LandsatConstants.*; /** * A helper class to parse Landsat metadata to build appropriate {@linkplain DefaultMetadata ISO19115 Metadata} * and also define if the current analysed file is {@linkplain #isValid() is valid} to build an appropriate * Landsat {@link Coverage} from its metadata file. * * @author Remi Marechal (Geomatys) * @version 1.0 * @since 1.0 */ public class LandsatMetadataParser { private static final Logger LOGGER = Logging.getLogger("org.geotoolkit.coverage"); /** * {@link Path} link to Landsat Metadatas. */ private final Path metadataPath; /** * The metadata ISO19115. * */ private DefaultMetadata isoMetadata, reflectiveMetadatas, panchromaticMetadatas, thermalMetadatas; /** * {@link Map} which regroup all read metadatas fields. */ private final Map<String, String> metaGroups; /** * 2D part of Projected CRS. */ private CoordinateReferenceSystem projectedCRS2D; /** * Multidimensionnal Projected CRS. */ private CoordinateReferenceSystem projectedCRS; /** * The acquisition data {@link Date}. * */ private Date date; /** * Build a metadata parser for Landsat. * * Read the metadata file with the {@link StandardCharsets#US_ASCII} default charset. * * @param metadataPath path where the metadata file is stored. * @throws IOException */ public LandsatMetadataParser(final Path metadataPath) throws IOException { this(metadataPath, StandardCharsets.US_ASCII); } /** * Build a metadata parser for Landsat. * * @param metadataPath path where the metadata file is stored. * @param charset * @throws IOException */ public LandsatMetadataParser(final Path metadataPath, final Charset charset) throws IOException { ArgumentChecks.ensureNonNull("metadata Path", metadataPath); if (!Files.exists(metadataPath)){ throw new IllegalArgumentException("metadata Path doesn't exist at path : "+metadataPath.toString()); } this.metadataPath = metadataPath; metaGroups = getMetadataGroups(charset); } /** * Travel metadatas and return {@code true} if metadata are adapted for Landsat * reading behavior else return {@code false}.<br><br> * * Validity criterion are :<br> * - band 2, 3 and 4 are referenced for RGB.<br> * - ellipsoid, datum, utm area for projected CRS.<br> * - latitude and longitudinal coordinates values for projected bounding box. * * @return {@code true} if minimum metadatas values are referenced else return {@code false}. */ public final boolean isValid() { //-- check minimum band existence. for (int i = 2; i < 5; i++) { //-- mandatory false to avoid unexpected search exception. final String value = getValue(false, "FILE_NAME_BAND_"+i); if (value == null) return false; } //-- crs labels //-- Datum final String datum = getValue(false, "DATUM"); if (datum == null) return false; //-- Ellipsoid final String ellips = getValue(false, "ELLIPSOID"); if (ellips == null) return false; //-- Map Projection Type final String mapProj = getValue(false, "MAP_PROJECTION"); if (mapProj == null) return false; //-- projected Bounding box coordinates values. //-- longitude final String projWest = getValue(true, "CORNER_LL_PROJECTION_X_PRODUCT", "CORNER_UL_PROJECTION_X_PRODUCT"); if (projWest == null) return false; final String projEst = getValue(true, "CORNER_UR_PROJECTION_X_PRODUCT", "CORNER_LR_PROJECTION_X_PRODUCT"); if (projEst == null) return false; //-- lattitude final String projSouth = getValue(true, "CORNER_LL_PROJECTION_Y_PRODUCT", "CORNER_LR_PROJECTION_Y_PRODUCT"); if (projSouth == null) return false; final String projNorth = getValue(true, "CORNER_UR_PROJECTION_Y_PRODUCT", "CORNER_UL_PROJECTION_Y_PRODUCT"); if (projNorth == null) return false; return true; } /** * Returns Landsat ISO19115 metadatas. * * @return * @throws FactoryException * @throws ParseException */ public final DefaultMetadata getMetadata(final String groupName) throws FactoryException, ParseException { ArgumentChecks.ensureNonNull("Metadata group name", groupName); final DefaultMetadata filledMetadata = new DefaultMetadata(); switch(groupName) { case GENERAL_LABEL : { if (isoMetadata != null) { return isoMetadata; } else { isoMetadata = filledMetadata; } break; } case REFLECTIVE_LABEL : { if (reflectiveMetadatas != null) { return reflectiveMetadatas; } else { reflectiveMetadatas = filledMetadata; } break; } case PANCHROMATIC_LABEL : { if (panchromaticMetadatas != null) { return panchromaticMetadatas; } else { panchromaticMetadatas = filledMetadata; } break; } case THERMAL_LABEL : { if (thermalMetadatas != null) { return thermalMetadatas; } else { thermalMetadatas = filledMetadata; } break; } } assert metaGroups != null; //----------------------------------------------------------------------// //------------------------ Mandatory metadata --------------------------// //----------------------------------------------------------------------// //-- set CRS filledMetadata.setReferenceSystemInfo(Collections.singleton(getCRS())); final Date metadataPublicationDate = getDateInfo(); if (metadataPublicationDate != null) filledMetadata.setDateStamp(metadataPublicationDate); //-- unique file identifier filledMetadata.setFileIdentifier(UUID.randomUUID().toString()); //-- Iso metadatas 19115 generation date. filledMetadata.setDateStamp(new Date()); //-- set bounding box final double[] bbCoords = getProjectedBound2D(); final DefaultGeographicBoundingBox geo = new DefaultGeographicBoundingBox(bbCoords[0], bbCoords[1], //-- long bbCoords[2], bbCoords[3]); //-- lat final DefaultExtent ex = new DefaultExtent(); ex.setGeographicElements(Arrays.asList(geo)); //-- acquisition date final DefaultTemporalExtent tex = new DefaultTemporalExtent(); final Date acquisitionDate = getAcquisitionDate(); tex.setBounds(acquisitionDate, acquisitionDate); ex.setTemporalElements(Arrays.asList(tex)); //-- temporal extent final NamedIdentifier extentName = new NamedIdentifier(Citations.CRS, "Landsat extent"); final Map<String, Object> propertiesExtent = new HashMap<>(); propertiesExtent.put(IdentifiedObject.NAME_KEY, extentName); final NamedIdentifier extentBeginName = new NamedIdentifier(Citations.CRS, "Landsat extent"); final Map<String, Object> propertiesBegin = new HashMap<>(); propertiesBegin.put(IdentifiedObject.NAME_KEY, extentBeginName); final NamedIdentifier extentEnd = new NamedIdentifier(Citations.CRS, "Landsat extent"); final Map<String, Object> propertiesEnd = new HashMap<>(); propertiesEnd.put(IdentifiedObject.NAME_KEY, extentEnd); tex.setExtent(new DefaultPeriod(propertiesExtent, new DefaultInstant(propertiesBegin, acquisitionDate), new DefaultInstant(propertiesEnd, acquisitionDate))); //-- all minimum mandatory metadatas. //-- geographic extent final DefaultDataIdentification ddii = new DefaultDataIdentification(); ddii.setExtents(Arrays.asList(ex)); //-- comment about data final String abstractComment = getValue(true, "ORIGIN"); ddii.setAbstract(new DefaultInternationalString(abstractComment)); //-- scene title final String title = "Generales Landsat8 metadatas for : "+getValue(true, LandsatConstants.SCENE_ID); final DefaultCitation titleCitation = new DefaultCitation(title); //-- dates Set<DefaultCitationDate> dateset = new HashSet<>(); dateset.add(new DefaultCitationDate(acquisitionDate, DateType.CREATION)); dateset.add(new DefaultCitationDate(metadataPublicationDate, DateType.PUBLICATION)); titleCitation.setDates(dateset); ddii.setCitation(titleCitation); //-- Resolution if (!groupName.equalsIgnoreCase(GENERAL_LABEL)) { final String reres = getValue(false, RESOLUTION_LABEL+groupName); if (reres != null) { HashSet<Resolution> res = new HashSet<Resolution>(); final DefaultResolution defaultResolution = new DefaultResolution(); defaultResolution.setDistance(Double.valueOf(reres)); res.add(defaultResolution); ddii.setSpatialResolutions(res); } } filledMetadata.setIdentificationInfo(Arrays.asList(ddii)); /** * Three different Images Descriptions. * - Reflective * - Panchromatic * - Thermal */ //-- Reflective description. final DefaultImageDescription reflectiveImgDesc = new DefaultImageDescription(); final DefaultAttributeGroup dAGReflectiveRef = new DefaultAttributeGroup(); dAGReflectiveRef.setAttributes(getBandsInfos("REFLECTIVE", "REFLECTANCE")); final DefaultAttributeGroup dAGReflectiveRad = new DefaultAttributeGroup(); dAGReflectiveRad.setAttributes(getBandsInfos("REFLECTIVE", "RADIANCE")); final Set<AttributeGroup> reflectiveInfos = new HashSet<>(); reflectiveInfos.add(dAGReflectiveRef); reflectiveInfos.add(dAGReflectiveRad); reflectiveImgDesc.setAttributeGroups(reflectiveInfos); //-- Panchromatic image description. final DefaultImageDescription panchroImgDesc = new DefaultImageDescription(); final DefaultAttributeGroup dAGPanchromaRef = new DefaultAttributeGroup(); dAGPanchromaRef.setAttributes(getBandsInfos("PANCHROMATIC", "REFLECTANCE")); final DefaultAttributeGroup dAGPanchromaRad = new DefaultAttributeGroup(); dAGPanchromaRad.setAttributes(getBandsInfos("PANCHROMATIC", "RADIANCE")); final Set<AttributeGroup> panchroInfos = new HashSet<>(); panchroInfos.add(dAGPanchromaRef); panchroInfos.add(dAGPanchromaRad); panchroImgDesc.setAttributeGroups(panchroInfos); //-- Thermal descriptions. (only define with Radiance) final DefaultImageDescription thermalImgDesc = new DefaultImageDescription(); final DefaultAttributeGroup dAGThermalRad = new DefaultAttributeGroup(); dAGThermalRad.setAttributes(getBandsInfos("THERMAL", "RADIANCE")); thermalImgDesc.setAttributeGroups(Collections.singleton(dAGThermalRad)); //-- image description final String cloud = getValue(false, "CLOUD_COVER"); if (cloud != null) { final double val = Double.valueOf(cloud); reflectiveImgDesc.setCloudCoverPercentage(val); panchroImgDesc.setCloudCoverPercentage(val); thermalImgDesc.setCloudCoverPercentage(val); } final String sunAz = getValue(false, "SUN_AZIMUTH"); if (sunAz != null) { final double val = Double.valueOf(sunAz); reflectiveImgDesc.setIlluminationAzimuthAngle(val); panchroImgDesc.setIlluminationAzimuthAngle(val); thermalImgDesc.setIlluminationAzimuthAngle(val); } final String sunEl = getValue(false, "SUN_ELEVATION"); if (sunEl != null) { final double val = Double.valueOf(sunEl); reflectiveImgDesc.setIlluminationElevationAngle(val); panchroImgDesc.setIlluminationElevationAngle(val); thermalImgDesc.setIlluminationElevationAngle(val); } final HashSet<ImageDescription> imgDescriptions = new HashSet<>(); switch (groupName) { case REFLECTIVE_LABEL : imgDescriptions.add(reflectiveImgDesc);break; case PANCHROMATIC_LABEL : imgDescriptions.add(panchroImgDesc);break; case THERMAL_LABEL : imgDescriptions.add(thermalImgDesc);break; default : { imgDescriptions.add(reflectiveImgDesc); imgDescriptions.add(panchroImgDesc); imgDescriptions.add(thermalImgDesc); } } filledMetadata.setContentInfo(imgDescriptions); //----------------------------------------------------------------------// //------------------------- optional metadatas -------------------------// //----------------------------------------------------------------------// //-- set metadata Date publication filledMetadata.setDateInfo(Collections.singleton(new DefaultCitationDate(metadataPublicationDate, DateType.PUBLICATION))); //-- Distribution informations final DefaultDistribution distribution = new DefaultDistribution(); final String origin = getValue(false, "ORIGIN"); if (origin != null) distribution.setDescription(new DefaultInternationalString(origin)); final String outputFormat = getValue(false, "OUTPUT_FORMAT"); final String processSoftVersion = getValue(false, "PROCESSING_SOFTWARE_VERSION"); if ((outputFormat != null) && (processSoftVersion != null)){ distribution.setDistributionFormats(Collections.singleton(new DefaultFormat(outputFormat, processSoftVersion))); } filledMetadata.setDistributionInfo(Collections.singleton(distribution)); //-- Aquisition informations final DefaultAcquisitionInformation dAI = new DefaultAcquisitionInformation(); //-- platform final DefaultPlatform platform = new DefaultPlatform(); final String platF = getValue(false, "SPACECRAFT_ID"); if (platF != null){ platform.setCitation(new DefaultCitation()); } //-- instrument final DefaultInstrument instru = new DefaultInstrument(); final String instrum = getValue(false, "SENSOR_ID"); if (instrum != null) { instru.setType(new DefaultInternationalString(instrum)); } if (platF != null && instrum != null) { //-- set related founded instrument and platform //*****************************************************************// //-- a cycle is define here, platform -> instru and instru -> platform //-- like a dad know his son and a son know his dad. //-- during xml binding a cycle is not supported for the current Apach SIS version //-- decomment this row when upgrade SIS version //instru.setMountedOn(platform); //*****************************************************************// platform.setInstruments(Collections.singleton(instru)); dAI.setPlatforms(Collections.singleton(platform)); dAI.setInstruments(Collections.singleton(instru)); filledMetadata.setAcquisitionInformation(Collections.singleton(dAI)); } //-- additionnal informations about thermic band metadatas. if (groupName.equalsIgnoreCase(THERMAL_LABEL)) { final Set<ProcessStep> extendedInfos = getThermicInfos(); if (!extendedInfos.isEmpty()) { final DefaultLineage defaultLineage = new DefaultLineage(); defaultLineage.setProcessSteps(extendedInfos); filledMetadata.setResourceLineages(Collections.singleton(defaultLineage)); } } return filledMetadata; } /** * Return a map of the metadata fields. * @return */ public Map<String, String> getKeyValueMap(){ return Collections.unmodifiableMap(metaGroups); } //-------------------------------------------------------------------------// // Mandatory metadatas // //-------------------------------------------------------------------------// /** * Returns {@link Collections} of Landsat Reflective {@link Band}. * Means metadatas about band (1 to 7 and 9). * * @param groupName name of group, expected name are : PANCHROMATIC or REFLECTIVE or THERMAL. * @param reflectanceOrRadiance band metadata for REFLECTANCE or RADIANCE band attributs. * @return {@link Collections} of Landsat Reflective {@link Band}. */ private Set<DefaultBand> getBandsInfos(final String groupName, final String reflectanceOrRadiance) { int[] indexBands = null; switch(groupName) { case PANCHROMATIC_LABEL : { indexBands = new int[]{8}; break; } case REFLECTIVE_LABEL : { indexBands = new int[]{1, 2, 3, 4, 5, 6, 7, 9}; break; } case THERMAL_LABEL : { indexBands = new int[]{10, 11}; break; } default : throw new IllegalStateException("Group Name should be : PANCHROMATIC or REFLECTIVE or THERMAL."); } return getBandsInfos(groupName, reflectanceOrRadiance, indexBands); } /** * Returns all bands informations from metadata text file. * * @param groupName * @param reflectanceOrRadiance * @param bandsIndex * @return */ private Set<DefaultBand> getBandsInfos(final String groupName, final String reflectanceOrRadiance, final int ...bandsIndex) { final HashSet<DefaultBand> bands = new HashSet<>(); for (int i : bandsIndex) { bands.add(getBandInfos(i, groupName, reflectanceOrRadiance)); } return bands; } /** * Build an appropriate metadatas {@link DefaultBand} properties. * * @param bandIndex Landsat band index. * @param reflectanceOrRadiance metadata for RADIANCE or REFLECTANCE case. * @return appropriate metadatas {@link DefaultBand} properties. */ private DefaultBand getBandInfos(final int bandIndex, final String groupName, final String reflectanceOrRadiance) { final DefaultBand df = new DefaultBand(); df.setTransferFunctionType(TransferFunctionType.LINEAR); final String reflectBandLabel = BAND_NAME_LABEL+bandIndex; final String reflectBandName = getValue(true, reflectBandLabel); df.setNames(Collections.singleton(new DefaultIdentifier(reflectBandName))); //-- minimum pixel band value final String minPBLabel = SAMPLE_MIN_LABEL+bandIndex; final String minPBVal = getValue(false, minPBLabel); if (minPBVal != null) df.setMinValue(Double.valueOf(minPBVal)); //-- maximum pixel band value final String maxPBLabel = SAMPLE_MAX_LABEL+bandIndex; final String maxPBVal = getValue(false, maxPBLabel); if (maxPBVal != null) df.setMaxValue(Double.valueOf(maxPBVal)); //-- scale factor final String sfRBLabel = reflectanceOrRadiance+SCALE_LABEL+bandIndex; final String sfRBVal = getValue(false, sfRBLabel); if (sfRBVal != null) df.setScaleFactor(Double.valueOf(sfRBVal)); //-- offset final String offRBLabel = reflectanceOrRadiance+OFFSET_LABEL+bandIndex; final String offRBVal = getValue(false, offRBLabel); if (offRBVal != null) df.setOffset(Double.valueOf(offRBVal)); //-- resolution final String gridCellSizeLabel = RESOLUTION_LABEL+groupName; final String gridCellSizeValue = getValue(false, gridCellSizeLabel); if (gridCellSizeValue != null) df.setNominalSpatialResolution(Double.valueOf(gridCellSizeValue)); return df; } /** * Extract from given metadatas {@link Map} needed value to compute an * appropriate2D part of {@link CoordinateReferenceSystem}. * * @param metaMap {@link Map} which contain all metadatas fields. * @return stipuled metadata CRS. * @throws FactoryException if impossible to compute CRS. */ private CoordinateReferenceSystem getCRS2D() throws FactoryException { if (projectedCRS2D != null) return projectedCRS2D; //-- Datum final String datum = getValue(true, "DATUM"); //-- Ellipsoid final String ellips = getValue(true, "ELLIPSOID"); if (!(("WGS84".equalsIgnoreCase(datum)) && ("WGS84".equalsIgnoreCase(ellips)))){ throw new IllegalStateException("Comportement not supported : expected Datum and Ellipsoid value WGS84, found Datum = "+datum+", Ellipsoid : "+ellips); } final String projType = getValue(true, "MAP_PROJECTION"); switch (projType) { case "UTM" : { /** * From Landsat specification, normaly Datum and ellipsoid are always WGS84. * UTM area is the only thing which change. * Thereby we build a CRS from basic 32600 and we add read UTM area. * For example if UTM area is 45 we decode 32645 CRS from EPSG database. */ final String utm_Zone = getValue(true, "UTM_ZONE"); final Integer utm = Integer.valueOf(utm_Zone); ArgumentChecks.ensureBetween(datum, 0, 60, utm); final NumberFormat nf = new DecimalFormat("##"); final String utmFormat = nf.format(utm); projectedCRS2D = CRS.forCode("EPSG:326"+utmFormat); break; } case "PS" : { final String originLongitude = getValue(true, "VERTICAL_LON_FROM_POLE"); final String trueLatitudeScale = getValue(true, "TRUE_SCALE_LAT"); final String falseEasting = getValue(true, "FALSE_EASTING"); final String falseNorthing = getValue(true, "FALSE_NORTHING"); final OperationMethod method = DefaultFactories.forBuildin(CoordinateOperationFactory.class) .getOperationMethod("Polar Stereographic (variant B)"); final ParameterValueGroup psParameters = method.getParameters().createValue(); psParameters.parameter(Constants.STANDARD_PARALLEL_1).setValue(Double.valueOf(trueLatitudeScale)); psParameters.parameter(Constants.CENTRAL_MERIDIAN).setValue(Double.valueOf(originLongitude)); psParameters.parameter(Constants.FALSE_EASTING).setValue(Double.valueOf(falseEasting)); psParameters.parameter(Constants.FALSE_NORTHING).setValue(Double.valueOf(falseNorthing)); final Map<String, String> properties = Collections.singletonMap("name", "Landsat 8 polar stereographic"); //-- define mathematical formula to pass from Geographic Base CRS to projected Coordinate space. final DefaultConversion projection = new DefaultConversion(properties, method, null, psParameters); projectedCRS2D = new DefaultProjectedCRS(properties, CommonCRS.WGS84.normalizedGeographic(), projection, PredefinedCS.PROJECTED); break; } default : throw new IllegalStateException("Comportement not supported : expected MAP_PROJECTION values are : PS or UTM, found : "+projType); } return projectedCRS2D; } /** * Returns the data acquisition {@link Date}.<br> * * May returns {@code null} if no date are stipulate from metadata file. * * @return the data acquisition {@link Date}. * @throws ParseException if problem during Date parsing. */ private Date getAcquisitionDate() throws ParseException { if (date != null) return date; /** * Get temporales acquisition informations. */ //-- year month day final String dateAcquired = getValue(false, "DATE_ACQUIRED"); if (dateAcquired == null) return null; //-- hh mm ss:ms final String sceneCenterTime = getValue(false, "SCENE_CENTER_TIME"); String strDate = dateAcquired; if (sceneCenterTime != null) strDate = dateAcquired+"T"+sceneCenterTime; date = TimeParser.toDate(strDate); return date; } /** * Returns the internaly extent bounding box coordinates. * Coordinates are organize as follow : {west, est, south, north}. * * @return the internaly extent bounding box coordinates. */ private double[] getProjectedBound2D() { //-- longitude final String lowWest = getValue(true, "CORNER_LL_LON_PRODUCT", "CORNER_UL_LON_PRODUCT"); final String upWest = getValue(true, "CORNER_UL_LON_PRODUCT", "CORNER_LL_LON_PRODUCT"); final String lowEst = getValue(true, "CORNER_LR_LON_PRODUCT", "CORNER_UR_LON_PRODUCT"); final String upEst = getValue(true, "CORNER_UR_LON_PRODUCT", "CORNER_LR_LON_PRODUCT"); final double west = Math.min(Double.valueOf(lowWest), Double.valueOf(upWest)); final double est = Math.max(Double.valueOf(lowEst), Double.valueOf(upEst)); //-- lattitude final String westSouth = getValue(true, "CORNER_LL_LAT_PRODUCT", "CORNER_LR_LAT_PRODUCT"); final String estSouth = getValue(true, "CORNER_LR_LAT_PRODUCT", "CORNER_LL_LAT_PRODUCT"); final String westNorth = getValue(true, "CORNER_UL_LAT_PRODUCT", "CORNER_UR_LAT_PRODUCT"); final String estNorth = getValue(true, "CORNER_UR_LAT_PRODUCT", "CORNER_UL_LAT_PRODUCT"); final double south = Math.min(Double.valueOf(westSouth), Double.valueOf(estSouth)); final double north = Math.max(Double.valueOf(westNorth), Double.valueOf(estNorth)); return new double[]{west, est, south, north}; } /** * Returns built {@link CoordinateReferenceSystem} from metadata file.<br> * * Note : if no Date are specifiedthereturned CRS is the same than the {@link #getCRS2D() } result. * * @return {@link CoordinateReferenceSystem} from metadata file. * @throws FactoryException if impossible to compute CRS. */ CoordinateReferenceSystem getCRS() throws FactoryException { if (projectedCRS != null) { return projectedCRS; } final CoordinateReferenceSystem crs2D = getCRS2D(); //-- add temporal part if Date exist final TemporalCRS temporalCRS = CommonCRS.Temporal.JAVA.crs(); projectedCRS = new GeodeticObjectBuilder() .addName(crs2D.getName().getCode() + '/' + temporalCRS.getName().getCode()) .createCompoundCRS(crs2D, temporalCRS); return projectedCRS; } /** * Returns the internal Grid Extent from metadata for given Landsat group (Reflective, Panchromatic or Thermal). * * @param groupName Reflective, Panchromatic or Thermal * @return Grid Extent from metadata for given Landsat group * @throws FactoryException if impossible to compute CRS. */ GridEnvelope getGridExtent(final String groupName) throws FactoryException { //-- grid coordinates final String width = getValue(true, groupName+SAMPLES_LABEL); final String height = getValue(true, groupName+LINES_LABEL); final CoordinateSystem sysAxxes = getCRS().getCoordinateSystem(); final int dim = sysAxxes.getDimension(); final int[] upper = new int[dim]; Arrays.fill(upper, 1); upper[0] = Integer.valueOf(width); upper[1] = Integer.valueOf(height); return new GeneralGridEnvelope(new int[dim], upper, false); } /** * Build {@linkplain MathTransform GridToCRS} from internal grid extent, * fourth projected envelope corners and also {@link Date} if exist. * * @param groupName Reflective, Panchromatic or Thermal * @return Grid to CRS from metadata for given Landsat group * @throws ParseException if problem during Date parsing. * @throws FactoryException if impossible to compute CRS. */ MathTransform getGridToCRS(final String groupName) throws ParseException, FactoryException { final Date acquisitionDate = getAcquisitionDate(); final MathTransform gridToCRS2D = MathTransforms.linear(AffineTransforms2D.toMatrix(getGridToCRS2D(groupName))); if (acquisitionDate == null) return gridToCRS2D; final LinearTransform linearTime = MathTransforms.linear(0, acquisitionDate.getTime()); return MathTransforms.compound(gridToCRS2D, linearTime); } /** * Build grid to CRS from internal grid extent and fourth projected envelope corners. * The returned {@link AffineTransform} is built from {@link LocalizationGrid#getAffineTransform() }. * * @param groupName Reflective, Panchromatic or Thermal * @return Grid to CRS from metadata for given Landsat group * @throws FactoryException if impossible to compute CRS. */ private AffineTransform getGridToCRS2D(final String groupName) throws FactoryException { final GridEnvelope gridExtent = getGridExtent(groupName); //-- longitude final String lowWest = getValue(true, "CORNER_LL_LON_PRODUCT", "CORNER_UL_LON_PRODUCT"); final String upWest = getValue(true, "CORNER_UL_LON_PRODUCT", "CORNER_LL_LON_PRODUCT"); final String lowEst = getValue(true, "CORNER_LR_LON_PRODUCT", "CORNER_UR_LON_PRODUCT"); final String upEst = getValue(true, "CORNER_UR_LON_PRODUCT", "CORNER_LR_LON_PRODUCT"); //-- lattitude final String westSouth = getValue(true, "CORNER_LL_LAT_PRODUCT", "CORNER_LR_LAT_PRODUCT"); final String estSouth = getValue(true, "CORNER_LR_LAT_PRODUCT", "CORNER_LL_LAT_PRODUCT"); final String westNorth = getValue(true, "CORNER_UL_LAT_PRODUCT", "CORNER_UR_LAT_PRODUCT"); final String estNorth = getValue(true, "CORNER_UR_LAT_PRODUCT", "CORNER_UL_LAT_PRODUCT"); final double minLong = Math.min(Double.valueOf(lowWest), Double.valueOf(upWest)); final double spanLong = Math.max(Double.valueOf(lowEst), Double.valueOf(upEst)) - minLong; final double maxLat = Math.max(Double.valueOf(westNorth), Double.valueOf(estNorth)); final double spanLat = maxLat - Math.min(Double.valueOf(westSouth), Double.valueOf(estSouth)); return new AffineTransform2D(spanLong / gridExtent.getSpan(0), 0, 0, - spanLat / gridExtent.getSpan(1), minLong, maxLat); } /** * Returns Landsat projected {@link Envelope} with the additionnal temporal * axis related with the {@linkplain #getAcquisitionDate() date} image creation. * * @return projected {@link Envelope}. * @throws FactoryException if problem during CRS decoding * @throws ParseException if problem during date parsing */ Envelope getProjectedEnvelope() throws FactoryException, ParseException { final CoordinateReferenceSystem projCRS = getCRS(); assert projCRS != null; //-- {west, est, south, north} final double[] projectedBound2D = getProjectedBound2D(); final GeneralEnvelope projEnvelope = new GeneralEnvelope(projCRS); projEnvelope.setRange(0, projectedBound2D[0], projectedBound2D[1]); projEnvelope.setRange(1, projectedBound2D[2], projectedBound2D[3]); final Date dat = getAcquisitionDate(); if (dat != null) projEnvelope.setRange(2, dat.getTime(), dat.getTime()); return projEnvelope; } /** * Returns related metadata {@link Path}. * * @return metadata path. */ Path getPath(){ return metadataPath; } //-------------------------------------------------------------------------// // Optional metadatas // //-------------------------------------------------------------------------// /** * Returns the metadata date built. * * @return the metadata date built. */ private Date getDateInfo() throws ParseException { final String dateInfo = getValue(false, "FILE_DATE"); return TimeParser.toDate(dateInfo); } /** * Returns if exist K1 and K2 thermic formula constants. * May return {@code null}. * * @return if exist K1 and K2 thermic formula constants. */ private Set<ProcessStep> getThermicInfos() { final HashSet<ProcessStep> extendedInfos = new HashSet<>(); final ProcessStep k110 = getKConstant(1, 10); if (k110 != null) extendedInfos.add(k110); final ProcessStep k210 = getKConstant(2, 10); if (k210 != null) extendedInfos.add(k210); final ProcessStep k111 = getKConstant(1, 11); if (k111 != null) extendedInfos.add(k111); final ProcessStep k211 = getKConstant(2, 11); if (k211 != null) extendedInfos.add(k211); return extendedInfos; } /** * Returns if exist K constant for one thermic band. * * @param kVal 1 or 2 for K1 or K2. * @param bandIndex thermic band index. (10 or 11 for Landsat) * @return */ private ProcessStep getKConstant(final int kVal, final int bandIndex) { final String kLabel = "K"+kVal+"_CONSTANT_BAND_"+bandIndex; final String k = getValue(false, kLabel); if (k != null) { final DefaultProcessStep kProcessStep = new DefaultProcessStep(); final String description = kLabel+" = "+k; kProcessStep.setDescription(new DefaultInternationalString(description)); return kProcessStep; } return null; } //-------------------------------------------------------------------------// // Utility methods // //-------------------------------------------------------------------------// /** * Returns the same thing than {@link #getValue(boolean, java.lang.String, java.lang.String) } * with fallback label at {@code null} value. * * @param isMandatory define log or throw exception if value is not founded. * @param label the requested metadata field name. * @return requested value from given metadata field name. */ String getValue(final boolean isMandatory, final String label) { return getValue(isMandatory, label, null); } /** * * @param isMandatory * @param label * @param labelRescue * @return */ String getValue(final boolean isMandatory, final String label, final String labelRescue) { ArgumentChecks.ensureNonNull("Label", label); String labelValue = metaGroups.get(label); if (labelValue == null) { final String labelRescueValue = (labelRescue == null) ? null : metaGroups.get(labelRescue); if (labelRescueValue == null) { final String errorLabel = (labelRescue == null) ? label : label+" and "+labelRescue; if (isMandatory) { throw new IllegalStateException("Landsat "+errorLabel+" metadata " + "informations are missing impossible to define appropriate value."); } else { LOGGER.log(Level.FINEST, "The metadata label(s) : {0} is(are) missing.", errorLabel); } } labelValue = labelRescueValue; } return (labelValue != null) ? labelValue.toUpperCase(Locale.US) : null; } /** * Travel all metadata file and fill a {@link String} map to access more easily to metadatas values. * * @return * @throws IOException if problem during file reading. */ private Map<String, String> getMetadataGroups(final Charset charset) throws IOException { final Map<String, String> metaGroup = new HashMap<>(); final Iterator<String> iterator = Files.readAllLines(metadataPath, charset).iterator(); while (iterator.hasNext()) { final String currentValue = iterator.next(); final int id = currentValue.indexOf("="); if (id > 0) { final String key = currentValue.substring(0, id).trim().toUpperCase(); String value = currentValue.substring(id+1, currentValue.length()).trim(); if (value.startsWith("\"")) { value = value.substring(1, value.length()-1); } metaGroup.put(key, value); } } return metaGroup; } }