| /* |
| * 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.coverage.grid; |
| |
| import java.util.Map; |
| import java.util.HashMap; |
| import java.util.Locale; |
| import org.opengis.util.FactoryException; |
| import org.opengis.util.InternationalString; |
| import org.opengis.metadata.spatial.DimensionNameType; |
| import org.opengis.parameter.ParameterValueGroup; |
| import org.opengis.parameter.ParameterDescriptor; |
| import org.opengis.parameter.ParameterDescriptorGroup; |
| import org.opengis.referencing.IdentifiedObject; |
| import org.opengis.referencing.cs.CSFactory; |
| import org.opengis.referencing.cs.AxisDirection; |
| import org.opengis.referencing.cs.CoordinateSystem; |
| import org.opengis.referencing.cs.CoordinateSystemAxis; |
| import org.opengis.referencing.crs.CoordinateReferenceSystem; |
| import org.opengis.referencing.crs.CRSFactory; |
| import org.opengis.referencing.crs.SingleCRS; |
| import org.opengis.referencing.crs.CompoundCRS; |
| import org.opengis.referencing.crs.DerivedCRS; |
| import org.opengis.referencing.crs.EngineeringCRS; |
| import org.opengis.referencing.operation.Matrix; |
| import org.opengis.referencing.operation.Conversion; |
| import org.opengis.referencing.operation.OperationMethod; |
| import org.opengis.referencing.operation.MathTransform; |
| import org.opengis.referencing.operation.NoninvertibleTransformException; |
| import org.apache.sis.metadata.iso.extent.DefaultExtent; |
| import org.apache.sis.metadata.iso.citation.Citations; |
| import org.apache.sis.parameter.ParameterBuilder; |
| import org.apache.sis.referencing.CommonCRS; |
| import org.apache.sis.referencing.NamedIdentifier; |
| import org.apache.sis.referencing.cs.AbstractCS; |
| import org.apache.sis.referencing.crs.DefaultDerivedCRS; |
| import org.apache.sis.referencing.operation.DefaultConversion; |
| import org.apache.sis.referencing.operation.DefaultOperationMethod; |
| import org.apache.sis.referencing.operation.transform.TransformSeparator; |
| import org.apache.sis.referencing.factory.GeodeticObjectFactory; |
| import org.apache.sis.referencing.privy.AxisDirections; |
| import org.apache.sis.feature.internal.Resources; |
| import org.apache.sis.util.Characters; |
| import org.apache.sis.util.Classes; |
| import org.apache.sis.util.resources.Vocabulary; |
| import org.apache.sis.util.resources.Errors; |
| import org.apache.sis.util.iso.Types; |
| import org.apache.sis.measure.Units; |
| |
| |
| /** |
| * Builder for coordinate reference system which is derived from the coverage CRS by the inverse |
| * of the "grid to CRS" transform. Those CRS describe coordinates associated to the grid extent. |
| * This class provides two factory methods: |
| * |
| * <ul> |
| * <li>{@link #forCoverage(String, GridGeometry)}</li> |
| * <li>{@link #forExtentAlone(Matrix, DimensionNameType[])}</li> |
| * </ul> |
| * |
| * @author Martin Desruisseaux (IRD, Geomatys) |
| */ |
| final class GridExtentCRS { |
| /** |
| * Name of the parameter where to store the grid coverage name. |
| */ |
| private static final String NAME_PARAM = "Target grid name"; |
| |
| /** |
| * Name of the parameter specifying the way image indices are |
| * associated with the coverage data attributes. |
| */ |
| private static final String ANCHOR_PARAM = "Pixel in cell"; |
| |
| /** |
| * Description of "CRS to grid indices" operation method. |
| */ |
| private static final OperationMethod METHOD; |
| static { |
| final ParameterBuilder b = new ParameterBuilder().setRequired(true); |
| final ParameterDescriptor<?> name = b.addName(NAME_PARAM).create(String.class, null); |
| final ParameterDescriptor<?> anchor = b.addName(ANCHOR_PARAM).create(PixelInCell.class, PixelInCell.CELL_CENTER); |
| final ParameterDescriptorGroup params = b.addName("CRS to grid indices").createGroup(name, anchor); |
| METHOD = new DefaultOperationMethod(properties(params.getName()), params); |
| } |
| |
| /** |
| * Scope of usage of the CRS. |
| * This is a localized text saying "Conversions from coverage CRS to grid cell indices." |
| */ |
| private static final InternationalString SCOPE = Resources.formatInternational(Resources.Keys.CrsToGridConversion); |
| |
| /** |
| * Name of the coordinate systems created by this class. |
| */ |
| private static final NamedIdentifier CS_NAME = new NamedIdentifier( |
| Citations.SIS, Vocabulary.formatInternational(Vocabulary.Keys.GridExtent)); |
| |
| /** |
| * Do not allow instantiation of this class. |
| */ |
| private GridExtentCRS() { |
| } |
| |
| /** |
| * Creates a derived CRS for the grid extent of a grid coverage. |
| * |
| * <h4>Limitation</h4> |
| * If the CRS is compound, then this method takes only the first single CRS element. |
| * This is a restriction imposed by {@link DerivedCRS} API. |
| * As a result, the returned CRS may cover only the 2 or 3 first grid dimensions. |
| * |
| * @param name name of the CRS to create. |
| * @param gg grid geometry of the coverage. |
| * @param anchor the cell part to map (center or corner). |
| * @param locale locale to use for axis names, or {@code null} for default. |
| * @return a derived CRS for coordinates (cell indices) associated to the grid extent. |
| * @throws FactoryException if an error occurred during the use of {@link CSFactory} or {@link CRSFactory}. |
| */ |
| static DerivedCRS forCoverage(final String name, final GridGeometry gg, final PixelInCell anchor, final Locale locale) |
| throws FactoryException, NoninvertibleTransformException |
| { |
| /* |
| * Get the first `SingleCRS` instance (see "limitations" in method javadoc). |
| */ |
| CoordinateReferenceSystem crs = gg.getCoordinateReferenceSystem(); |
| boolean reduce = false; |
| while (!(crs instanceof SingleCRS)) { |
| if (!(crs instanceof CompoundCRS)) { |
| throw unsupported(locale, crs); |
| } |
| crs = ((CompoundCRS) crs).getComponents().get(0); |
| reduce = true; |
| } |
| /* |
| * If we took only a subset of CRS dimensions, take the same subset |
| * of "grid to CRS" dimensions and list of grid axes. |
| */ |
| MathTransform gridToCRS = gg.getGridToCRS(anchor); |
| DimensionNameType[] types = gg.getExtent().getAxisTypes(); |
| if (reduce) { |
| final TransformSeparator s = new TransformSeparator(gridToCRS); |
| s.addTargetDimensionRange(0, crs.getCoordinateSystem().getDimension()); |
| gridToCRS = s.separate(); |
| final int[] src = s.getSourceDimensions(); |
| final DimensionNameType[] allTypes = types; |
| types = new DimensionNameType[src.length]; |
| for (int i=0; i<src.length; i++) { |
| final int j = src[i]; |
| if (j < allTypes.length) { |
| types[i] = allTypes[j]; |
| } |
| } |
| } |
| /* |
| * Build the coordinate system assuming a null (identity) "grid to CRS" matrix |
| * because we are building the CS for the grid, not for the transformed envelope. |
| */ |
| final CoordinateSystem cs = createCS(gridToCRS.getSourceDimensions(), null, types, locale); |
| if (cs == null) { |
| throw unsupported(locale, crs); |
| } |
| /* |
| * Put everything together: parameters, conversion and finally the derived CRS. |
| */ |
| final var properties = new HashMap<String,Object>(8); |
| properties.put(IdentifiedObject.NAME_KEY, METHOD.getName()); |
| properties.put(DefaultConversion.LOCALE_KEY, locale); |
| properties.put(Conversion.SCOPE_KEY, SCOPE); |
| gg.getGeographicExtent().ifPresent((domain) -> { |
| properties.put(Conversion.DOMAIN_OF_VALIDITY_KEY, |
| new DefaultExtent(null, domain, null, null)); |
| }); |
| final ParameterValueGroup params = METHOD.getParameters().createValue(); |
| params.parameter(NAME_PARAM).setValue(name); |
| params.parameter(ANCHOR_PARAM).setValue(anchor); |
| final Conversion conversion = new DefaultConversion(properties, METHOD, gridToCRS.inverse(), params); |
| properties.put(IdentifiedObject.NAME_KEY, name); |
| return DefaultDerivedCRS.create(properties, (SingleCRS) crs, conversion, cs); |
| } |
| |
| /** |
| * Returns the exception to throw for an unsupported CRS. |
| */ |
| private static FactoryException unsupported(final Locale locale, final CoordinateReferenceSystem crs) { |
| return new FactoryException(Errors.forLocale(locale) |
| .getString(Errors.Keys.UnsupportedType_1, Classes.getShortClassName(crs))); |
| } |
| |
| /** |
| * Creates a properties map to give to CS, CRS or datum constructors. |
| */ |
| private static Map<String,?> properties(final Object name) { |
| return Map.of(IdentifiedObject.NAME_KEY, name); |
| } |
| |
| /** |
| * Creates a coordinate system axis of the given name. |
| */ |
| private static CoordinateSystemAxis axis(final CSFactory csFactory, final String name, |
| final String abbreviation, final AxisDirection direction) throws FactoryException |
| { |
| return csFactory.createCoordinateSystemAxis(properties(name), abbreviation, direction, Units.UNITY); |
| } |
| |
| /** |
| * Returns a default axis abbreviation for the given dimension. |
| */ |
| private static String abbreviation(final int dimension) { |
| final StringBuilder b = new StringBuilder(4).append('x').append(dimension); |
| for (int i=b.length(); --i >= 1;) { |
| b.setCharAt(i, Characters.toSuperScript(b.charAt(i))); |
| } |
| return b.toString(); |
| } |
| |
| /** |
| * Creates the coordinate system for engineering CRS. |
| * |
| * @param tgtDim number of dimensions of the coordinate system to create. |
| * @param gridToCRS matrix of the transform used for converting grid cell indices to envelope coordinates. |
| * It does not matter whether it maps pixel center or corner (translation coefficients are ignored). |
| * A {@code null} means to handle as an identity transform. |
| * @param types the value of {@link GridExtent#types} or a default value (shall not be {@code null}). |
| * @param locale locale to use for axis names, or {@code null} for default. |
| * @return coordinate system for the grid extent, or {@code null} if it cannot be inferred. |
| * @throws FactoryException if an error occurred during the use of {@link CSFactory}. |
| */ |
| private static CoordinateSystem createCS(final int tgtDim, final Matrix gridToCRS, |
| final DimensionNameType[] types, final Locale locale) throws FactoryException |
| { |
| int srcDim = types.length; // Used only for inspecting names. No need to be accurate. |
| if (gridToCRS != null) { |
| srcDim = Math.min(gridToCRS.getNumCol() - 1, srcDim); |
| } |
| final CoordinateSystemAxis[] axes = new CoordinateSystemAxis[tgtDim]; |
| final CSFactory csFactory = GeodeticObjectFactory.provider(); |
| boolean hasVertical = false; |
| boolean hasTime = false; |
| boolean hasOther = false; |
| for (int i=0; i<srcDim; i++) { |
| final DimensionNameType type = types[i]; |
| if (type != null) { |
| /* |
| * Try to locate the CRS dimension corresponding to grid dimension j. |
| * We expect a one-to-one matching; if it is not the case, return null. |
| * Current version does not accept scale factors, but we could revisit |
| * in a future version if there is a need for it. |
| */ |
| int target = i; |
| double scale = 0; |
| if (gridToCRS != null) { |
| target = -1; |
| for (int j=0; j<tgtDim; j++) { |
| final double m = gridToCRS.getElement(j, i); |
| if (m != 0) { |
| if (target >= 0 || axes[j] != null || Math.abs(m) != 1) { |
| return null; |
| } |
| target = j; |
| scale = m; |
| } |
| } |
| if (target < 0) { |
| return null; |
| } |
| } |
| /* |
| * This hard-coded set of axis directions is the converse of |
| * GridExtent.AXIS_DIRECTIONS map. |
| */ |
| String abbreviation; |
| AxisDirection direction; |
| if (type == DimensionNameType.COLUMN || type == DimensionNameType.SAMPLE) { |
| abbreviation = "x"; direction = AxisDirection.COLUMN_POSITIVE; |
| } else if (type == DimensionNameType.ROW || type == DimensionNameType.LINE) { |
| abbreviation = "y"; direction = AxisDirection.ROW_POSITIVE; |
| } else if (type == DimensionNameType.VERTICAL) { |
| abbreviation = "z"; direction = AxisDirection.UP; hasVertical = true; |
| } else if (type == DimensionNameType.TIME) { |
| abbreviation = "t"; direction = AxisDirection.FUTURE; hasTime = true; |
| } else { |
| abbreviation = abbreviation(target); |
| direction = AxisDirections.UNSPECIFIED; |
| hasOther = true; |
| } |
| /* |
| * Verify that no other axis has the same direction and abbreviation. If duplicated |
| * values are found, keep only the first occurrence in grid axis order (may not be |
| * the CRS axis order). |
| */ |
| for (int k = tgtDim; --k >= 0;) { |
| final CoordinateSystemAxis previous = axes[k]; |
| if (previous != null) { |
| if (direction.equals(AxisDirections.absolute(previous.getDirection()))) { |
| direction = AxisDirections.UNSPECIFIED; |
| hasOther = true; |
| } |
| if (abbreviation.equals(previous.getAbbreviation())) { |
| abbreviation = abbreviation(target); |
| } |
| } |
| } |
| if (scale < 0) { |
| direction = AxisDirections.opposite(direction); |
| } |
| final String name = Types.toString(Types.getCodeTitle(type), locale); |
| axes[target] = axis(csFactory, name, abbreviation, direction); |
| } |
| } |
| /* |
| * Search for axes that have not been created in above loop. |
| * It happens when some axes have no associated `DimensionNameType` code. |
| */ |
| for (int j=0; j<tgtDim; j++) { |
| if (axes[j] == null) { |
| final String name = Vocabulary.forLocale(locale).getString(Vocabulary.Keys.Dimension_1, j); |
| final String abbreviation = abbreviation(j); |
| axes[j] = axis(csFactory, name, abbreviation, AxisDirections.UNSPECIFIED); |
| } |
| } |
| /* |
| * Create a coordinate system of affine type if all axes seem spatial. |
| * If no specialized type seems to fit, use an unspecified ("abstract") |
| * coordinate system type in last resort. |
| */ |
| final Map<String,?> properties = properties(CS_NAME); |
| final CoordinateSystem cs; |
| if (hasOther || (tgtDim > (hasTime ? 1 : 3))) { |
| cs = new AbstractCS(properties, axes); |
| } else switch (tgtDim) { |
| case 1: { |
| final CoordinateSystemAxis axis = axes[0]; |
| if (hasVertical) { |
| cs = csFactory.createVerticalCS(properties, axis); |
| } else if (hasTime) { |
| cs = csFactory.createTimeCS(properties, axis); |
| } else { |
| cs = csFactory.createLinearCS(properties, axis); |
| } |
| break; |
| } |
| case 2: { |
| /* |
| * A null `gridToCRS` means that we are creating a CS for the grid, which is assumed a |
| * Cartesian space. A non-null value means that we are creating a CRS for a transformed |
| * envelope, in which case the CS type is not really known. |
| */ |
| cs = (gridToCRS == null) |
| ? csFactory.createCartesianCS(properties, axes[0], axes[1]) |
| : csFactory.createAffineCS (properties, axes[0], axes[1]); |
| break; |
| } |
| case 3: { |
| cs = (gridToCRS == null) |
| ? csFactory.createCartesianCS(properties, axes[0], axes[1], axes[2]) |
| : csFactory.createAffineCS (properties, axes[0], axes[1], axes[2]); |
| break; |
| } |
| default: { |
| cs = null; |
| break; |
| } |
| } |
| return cs; |
| } |
| |
| /** |
| * Builds the engineering coordinate reference system of a {@link GridExtent}. |
| * This is used only in the rare cases where we need to represent an extent as an envelope. |
| * This class converts {@link DimensionNameType} codes into axis names, abbreviations and directions. |
| * It is the converse of {@link GridExtent#typeFromAxes(CoordinateReferenceSystem, int)}. |
| * |
| * <p>The CRS type is always engineering. |
| * We cannot create temporal CRS because we do not know the temporal datum origin.</p> |
| * |
| * @param gridToCRS matrix of the transform used for converting grid cell indices to envelope coordinates. |
| * It does not matter whether it maps pixel center or corner (translation coefficients are ignored). |
| * @param types the value of {@link GridExtent#types} or a default value (shall not be {@code null}). |
| * @param locale locale to use for axis names, or {@code null} for default. |
| * @return CRS for the grid, or {@code null}. |
| * @throws FactoryException if an error occurred during the use of {@link CSFactory} or {@link CRSFactory}. |
| * |
| * @see GridExtent#typeFromAxes(CoordinateReferenceSystem, int) |
| */ |
| static EngineeringCRS forExtentAlone(final Matrix gridToCRS, final DimensionNameType[] types) |
| throws FactoryException |
| { |
| final CoordinateSystem cs = createCS(gridToCRS.getNumRow() - 1, gridToCRS, types, null); |
| if (cs == null) { |
| return null; |
| } |
| return GeodeticObjectFactory.provider().createEngineeringCRS( |
| properties(cs.getName()), CommonCRS.Engineering.GRID.datum(), cs); |
| } |
| } |