This is an automated email from the ASF dual-hosted git repository. asf-gitbox-commits pushed a commit to branch geoapi-4.0 in repository https://gitbox.apache.org/repos/asf/sis.git
commit 2d623b02d05dd2e101d2b89a14d55c033543ae47 Author: Martin Desruisseaux <[email protected]> AuthorDate: Sat Apr 25 00:51:20 2026 +0200 More robust creation of the CRS for grid coordinates: if we cannot create a DerivedCRS, fallback on EngineeringCRS. --- .../apache/sis/coverage/grid/GridCRSBuilder.java | 524 +++++++++++++++++++++ .../org/apache/sis/coverage/grid/GridExtent.java | 24 +- .../apache/sis/coverage/grid/GridExtentCRS.java | 413 ---------------- .../org/apache/sis/coverage/grid/GridGeometry.java | 39 +- .../apache/sis/coverage/grid/GridGeometryTest.java | 33 +- .../main/org/apache/sis/geometry/Envelopes.java | 2 +- .../apache/sis/referencing/IdentifiedObjects.java | 6 +- .../org/apache/sis/referencing/cs/AbstractCS.java | 2 +- .../apache/sis/referencing/cs/DefaultAffineCS.java | 2 +- .../sis/referencing/cs/DefaultCylindricalCS.java | 2 +- .../apache/sis/referencing/cs/DefaultLinearCS.java | 2 +- .../apache/sis/referencing/cs/DefaultPolarCS.java | 2 +- .../sis/referencing/cs/DefaultSphericalCS.java | 2 +- .../apache/sis/referencing/cs/DefaultTimeCS.java | 22 +- .../sis/referencing/cs/DefaultVerticalCS.java | 2 +- .../apache/sis/referencing/cs/package-info.java | 2 +- .../apache/sis/gui/referencing/AuthorityCodes.java | 15 +- .../org/apache/sis/gui/referencing/CRSChooser.java | 6 +- .../org/apache/sis/gui/referencing/MenuSync.java | 7 +- .../gui/referencing/RecentReferenceSystems.java | 114 ++--- 20 files changed, 682 insertions(+), 539 deletions(-) diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCRSBuilder.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCRSBuilder.java new file mode 100644 index 0000000000..48d5c14d38 --- /dev/null +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCRSBuilder.java @@ -0,0 +1,524 @@ +/* + * 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.Arrays; +import java.util.Collections; +import java.util.Map; +import java.util.HashMap; +import java.util.Locale; +import java.util.Optional; +import org.opengis.util.FactoryException; +import org.opengis.util.InternationalString; +import org.opengis.metadata.Identifier; +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.CompoundCRS; +import org.opengis.referencing.crs.DerivedCRS; +import org.opengis.referencing.crs.EngineeringCRS; +import org.opengis.referencing.operation.Matrix; +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.IdentifiedObjects; +import org.apache.sis.referencing.cs.AbstractCS; +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.factory.InvalidGeodeticParameterException; +import org.apache.sis.referencing.internal.shared.AxisDirections; +import org.apache.sis.referencing.internal.shared.ReferencingFactoryContainer; +import org.apache.sis.feature.internal.Resources; +import org.apache.sis.util.ArraysExt; +import org.apache.sis.util.Characters; +import org.apache.sis.util.resources.Vocabulary; +import org.apache.sis.util.iso.Types; +import org.apache.sis.measure.Units; + +// Specific to the geoapi-3.1 and geoapi-4.0 branches: +import org.opengis.referencing.ObjectDomain; + + +/** + * Builder for coordinate reference system which is derived from the coverage <abbr>CRS</abbr> + * by the inverse of the "grid to <abbr>CRS</abbr>" transform. Those <abbr>CRS</abbr> describe + * coordinates associated to the grid extent. This class provides two factory methods: + * + * <ul> + * <li>{@link #forCoverage()}</li> + * <li>{@link #forExtentAlone(Matrix, DimensionNameType[])}</li> + * </ul> + * + * @author Martin Desruisseaux (IRD, Geomatys) + */ +final class GridCRSBuilder extends ReferencingFactoryContainer { + /** + * 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 "<abbr>CRS</abbr> 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(Identifier.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(singleton(params.getName()), params); + } + + /** + * Scope of usage of the <abbr>CRS</abbr>. + * This is a localized text saying "Conversions from coverage <abbr>CRS</abbr> 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)); + + /** + * The grid geometry for which to create a grid coordinate reference system. + */ + private final GridGeometry grid; + + /** + * A helper tools for separating the "grid to <abbr>CRS</abbr>" transform for each component, + * or {@code null} if there is no transform. + */ + private final TransformSeparator separator; + + /** + * The cell part to map (center or corner). + */ + private final PixelInCell anchor; + + /** + * Name of the <abbr>CRS</abbr> to create. + */ + private final Identifier finalName; + + /** + * Properties to pass to the constructors of <abbr>CRS</abbr> components. Populated with metadata + * of potential interest except {@value IdentifiedObject#NAME_KEY}, which must be added before usage. + * + * @see #properties(Object) + */ + private final Map<String, Object> properties; + + /** + * Locale to use for axis names and error messages, or {@code null} for default. + */ + private final Locale locale; + + /** + * Creates a new helper class for building a grid coordinate reference system. + * + * @param grid grid geometry of the coverage. + * @param anchor the cell part to map (center or corner). + * @param name name of the <abbr>CRS</abbr> to create. + * @param derived whether to force {@link DerivedCRS} instances. + * @param locale locale to use for axis names and error messages, or {@code null} for default. + */ + GridCRSBuilder(final GridGeometry grid, final PixelInCell anchor, + final Identifier name, final boolean derived, final Locale locale) + { + this.grid = grid; + this.anchor = anchor; + this.locale = locale; + this.finalName = name; + this.properties = new HashMap<>(8); + properties.put(DefaultConversion.LOCALE_KEY, locale); + properties.put(ObjectDomain.SCOPE_KEY, SCOPE); + grid.getGeographicExtent().ifPresent((domain) -> { + properties.put(ObjectDomain.DOMAIN_OF_VALIDITY_KEY, new DefaultExtent(null, domain, null, null)); + }); + if (derived || grid.isDefined(GridGeometry.CRS | GridGeometry.GRID_TO_CRS)) { + separator = new TransformSeparator(grid.getGridToCRS(anchor)); + } else { + separator = null; + } + } + + /** + * Creates a derived or engineering <abbr>CRS</abbr> for the grid extent of a grid coverage. + * Derived <abbr>CRS</abbr> are preferred as they allow conversions to geospatial <abbr>CRS</abbr>. + * May return a compound <abbr>CRS</abbr> if the grid geometry has, for example, a temporal component. + * + * @return a derived, engineering or compound <abbr>CRS</abbr> for cell indices associated to the grid extent. + * @throws InvalidGeodeticParameterException if characteristics of the grid geometry disallow this operation. + * @throws FactoryException if another error occurred during the use of a referencing factory. + */ + final CoordinateReferenceSystem forCoverage() throws FactoryException { + if (separator != null) try { + return forComponent(finalName, grid.getCoordinateReferenceSystem(), 0, 0); + } catch (NoninvertibleTransformException e) { + throw new InvalidGeodeticParameterException(illegalGridToCRS(), e); + } + DimensionNameType[] types = null; + if (grid.isDefined(GridGeometry.EXTENT)) { + types = grid.getExtent().getAxisTypes(); + } + final int dimension = grid.getDimension(); + final CoordinateSystem cs = createGridCS(dimension, types, 0); + return getCRSFactory().createEngineeringCRS(properties(finalName), CommonCRS.Engineering.GRID.datum(), cs); + } + + /** + * Creates a derived <abbr>CRS</abbr> with a conversion from the grid geometry <abbr>CRS</abbr> + * to the grid extent <abbr>CRS</abbr>. This method can be invoked only when the grid geometry + * has a <abbr>CRS</abbr> and a "grid to <abbr>CRS</abbr>" transform. This case is identified + * by a non-null {@link #separator}. This method may invoke itself recursively. + * + * @param name name of the <abbr>CRS</abbr> to create, or {@code null} for a default value. + * @param baseCRS <abbr>CRS</abbr> or component of the <abbr>CRS</abbr> of the grid geometry. + * @param srcDim dimension of the first axis of {@code baseCRS}. Non-zero only during recursive invocation. + * @param tgtDim dimension of the first axis of the result. Non-zero only during recursive invocation. + * @return grid extent <abbr>CRS</abbr> derived from the given {@code baseCRS}. + * @throws FactoryException if an error occurred during the use of a referencing factory. + */ + private CoordinateReferenceSystem forComponent(Object name, final CoordinateReferenceSystem baseCRS, int srcDim, int tgtDim) + throws FactoryException, NoninvertibleTransformException + { + if (name == null) { + name = IdentifiedObjects.getSimpleNameOrIdentifier(baseCRS); + if (name == null) { + name = IdentifiedObjects.getDisplayName(baseCRS, locale); + } + name = "Grid of " + name; + } + final int dimension = baseCRS.getCoordinateSystem().getDimension(); + /* + * If the given CRS is a compound CRS (e.g. horizontal + vertical + temporal), + * invoke this method recursively for each component and assemble the result. + * We must keep in mind that the resulting CRS components are not necessarily + * in same order as the components of the real world CRS. + */ + if (baseCRS instanceof CompoundCRS) { + // At first, elements are duplicated in the `components` array for each axis. + final var components = new CoordinateReferenceSystem[dimension]; + for (final CoordinateReferenceSystem crs : ((CompoundCRS) baseCRS).getComponents()) { + final CoordinateReferenceSystem derived = forComponent(null, crs, srcDim, tgtDim); + for (int i : separator.getSourceDimensions()) components[i] = derived; + separator.clear(); + srcDim += crs.getCoordinateSystem().getDimension(); + tgtDim += derived.getCoordinateSystem().getDimension(); + } + // Deduplicate components with the restriction that same components must be consecutive. + int count = 1; + for (int i=1; i<dimension; i++) { + final CoordinateReferenceSystem crs = components[i]; + if (crs != components[i-1]) { + for (int j = count; --j >= 0;) { + if (components[j] == crs) { + throw new InvalidGeodeticParameterException(illegalGridToCRS()); + } + } + components[count++] = crs; + } + } + return getCRSFactory().createCompoundCRS(properties(name), ArraysExt.resize(components, count)); + } + /* + * Case of a single (non-compound) CRS. The separator contains the "grid to CRS" transform. + * Therefore, the target dimensions are the CRS dimensions, which are the source dimensions + * from the point of view of the derived CRS. + */ + separator.addTargetDimensionRange(srcDim, srcDim + dimension); + final MathTransform gridToCRS = separator.separate(); + final int[] src = separator.getSourceDimensions(); + final var types = new DimensionNameType[src.length]; + if (grid.isDefined(GridGeometry.EXTENT)) { + final GridExtent extent = grid.getExtent(); + final CoordinateSystem cs = baseCRS.getCoordinateSystem(); + Arrays.setAll(types, (i) -> extent.getAxisType(src[i]).orElseGet( + () -> GridExtent.typeFromAxis(cs.getAxis(i)).orElse(null))); + } + final CoordinateSystem cs = createGridCS(src.length, types, tgtDim); + final ParameterValueGroup params = METHOD.getParameters().createValue(); + params.parameter(NAME_PARAM).setValue(finalName); + params.parameter(ANCHOR_PARAM).setValue(anchor); + final var conversion = new DefaultConversion(properties(METHOD.getName()), METHOD, gridToCRS.inverse(), params); + return getCRSFactory().createDerivedCRS(properties(name), baseCRS, conversion, cs); + } + + /** + * Returns an error message for an illegal "grid to <abbr>CRS</abbr>" transform. + */ + private String illegalGridToCRS() { + return Resources.forLocale(locale).getString(Resources.Keys.IllegalGridGeometryComponent_1, "gridToCRS"); + } + + /** + * Returns the properties map for the construction of a <abbr>CRS</abbr> or operation of the given name. + */ + private Map<String,?> properties(final Object name) { + properties.put(IdentifiedObject.NAME_KEY, name); + return Collections.unmodifiableMap(properties); + } + + /** + * Creates a properties map for the construction of geodetic objects with only a name. + */ + private static Map<String,?> singleton(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(singleton(name), abbreviation, direction, Units.UNITY); + } + + /** + * Returns a default axis abbreviation for the given dimension. + */ + private static String abbreviation(final int dimension) { + final var b = new StringBuilder(4).append('x').append(dimension); + for (int i = b.length(); --i >= 1;) { + b.setCharAt(i, Characters.toSubScript(b.charAt(i))); + } + return b.toString(); + } + + /** + * Creates the coordinate system for a derived or engineering <abbr>CRS</abbr> of the grid. + * + * @param dimension number of dimensions of the coordinate system to create. + * @param types the value of {@link GridExtent#types} or a default value (shall not be {@code null}). + * @param offset offset to apply on dimension index when creating a default axis name. + * @return coordinate system for the grid extent (never {@code null}). + * @throws FactoryException if an error occurred during the use of {@link CSFactory}. + */ + private CoordinateSystem createGridCS(final int dimension, final DimensionNameType[] types, final int offset) + throws FactoryException + { + /* + * 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 extent. + */ + final CoordinateSystem cs = createCS(dimension, null, types, offset, getCSFactory(), locale); + if (cs == null) { + // Should never happen. + throw new InvalidGeodeticParameterException(); + } + return cs; + } + + /** + * Creates the coordinate system for the derived or engineering <abbr>CRS</abbr> of a grid. + * If the {@code gridToCRS} matrix is non-null, then the returned <abbr>CRS</abbr> is for + * the result of transforming an extent by that transform (it may change the axis order). + * + * @param dimension 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 (translations are ignored). + * A {@code null} value 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 offset offset to apply on dimension index when creating a default axis name. + * @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 dimension, final Matrix gridToCRS, + final DimensionNameType[] types, final int offset, + final CSFactory csFactory, final Locale locale) + throws FactoryException + { + final int numTypes = Math.min(types.length, dimension); + final var axes = new CoordinateSystemAxis[dimension]; + boolean hasVertical = false; + boolean hasTime = false; + boolean hasOther = false; + for (int i=0; i<numTypes; 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<dimension; 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 = AxisDirection.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 = dimension; --k >= 0;) { + final CoordinateSystemAxis previous = axes[k]; + if (previous != null) { + if (direction.equals(AxisDirections.absolute(previous.getDirection()))) { + direction = AxisDirection.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<dimension; j++) { + if (axes[j] == null) { + final int i = offset + j; + final String name = Vocabulary.forLocale(locale).getString(Vocabulary.Keys.Dimension_1, i); + final String abbreviation = abbreviation(i); + axes[j] = axis(csFactory, name, abbreviation, AxisDirection.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 = singleton(CS_NAME); + switch (dimension) { + case 1: { + final CoordinateSystemAxis axis = axes[0]; + if (hasVertical) { + return csFactory.createVerticalCS(properties, axis); + } else if (hasTime) { + return csFactory.createTimeCS(properties, axis); + } else if (hasOther) { + break; + } else { + return csFactory.createLinearCS(properties, axis); + } + } + 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. + */ + if (hasVertical | hasTime | hasOther) break; + return (gridToCRS == null) + ? csFactory.createCartesianCS(properties, axes[0], axes[1]) + : csFactory.createAffineCS (properties, axes[0], axes[1]); + } + case 3: { + if (hasVertical | hasTime | hasOther) break; + return (gridToCRS == null) + ? csFactory.createCartesianCS(properties, axes[0], axes[1], axes[2]) + : csFactory.createAffineCS (properties, axes[0], axes[1], axes[2]); + } + } + return new AbstractCS(properties, axes); + } + + /** + * Builds the coordinate reference system of the result of transforming 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 <abbr>CRS</abbr> type is always engineering. In particular, the <abbr>CRS</abbr> cannot be temporal + * because we do not know the temporal datum origin and because index unit is not a temporal unit.</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 (translations are ignored). + * @param types the value of {@link GridExtent#types} or a default value (shall not be {@code null}). + * @return <abbr>CRS</abbr> for the grid, or empty if it cannot be built. + * @throws FactoryException if an error occurred during the use of a referencing factory. + * + * @see GridExtent#toEnvelope(MathTransform) + * @see GridExtent#typeFromAxes(CoordinateReferenceSystem, int) + */ + static Optional<EngineeringCRS> forExtentAlone(final Matrix gridToCRS, final DimensionNameType[] types) + throws FactoryException + { + final GeodeticObjectFactory factory = GeodeticObjectFactory.provider(); + final CoordinateSystem cs = createCS(gridToCRS.getNumRow() - 1, gridToCRS, types, 0, factory, null); + if (cs == null) { + return Optional.empty(); + } + return Optional.of(factory.createEngineeringCRS(singleton(cs.getName()), CommonCRS.Engineering.GRID.datum(), cs)); + } +} diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridExtent.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridExtent.java index eca33676d7..8c3439f78d 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridExtent.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridExtent.java @@ -60,7 +60,6 @@ import org.apache.sis.geometry.AbstractEnvelope; import org.apache.sis.geometry.GeneralEnvelope; import org.apache.sis.geometry.Envelopes; import org.apache.sis.coverage.SubspaceNotSpecifiedException; -import org.apache.sis.referencing.internal.shared.AxisDirections; import org.apache.sis.referencing.internal.shared.ExtendedPrecisionMatrix; import org.apache.sis.referencing.operation.matrix.Matrices; import org.apache.sis.referencing.operation.matrix.MatrixSIS; @@ -113,7 +112,6 @@ public class GridExtent implements GridEnvelope, LenientComparable, Serializable /** * The dimension name types for given coordinate system axis directions. - * This map contains only the "positive" axis directions. * * @todo Verify if there is more directions to add as of ISO 19111:2018. * @@ -121,16 +119,20 @@ public class GridExtent implements GridEnvelope, LenientComparable, Serializable */ private static final Map<AxisDirection, DimensionNameType> AXIS_DIRECTIONS = Map.of( AxisDirection.COLUMN_POSITIVE, DimensionNameType.COLUMN, + AxisDirection.COLUMN_NEGATIVE, DimensionNameType.COLUMN, AxisDirection.ROW_POSITIVE, DimensionNameType.ROW, + AxisDirection.ROW_NEGATIVE, DimensionNameType.ROW, AxisDirection.UP, DimensionNameType.VERTICAL, - AxisDirection.FUTURE, DimensionNameType.TIME); + AxisDirection.DOWN, DimensionNameType.VERTICAL, + AxisDirection.FUTURE, DimensionNameType.TIME, + AxisDirection.PAST, DimensionNameType.TIME); /** * Default axis types for the two-dimensional cases. */ - private static final DimensionNameType[] DEFAULT_TYPES = new DimensionNameType[] { - DimensionNameType.COLUMN, - DimensionNameType.ROW + private static final DimensionNameType[] DEFAULT_TYPES = { + DimensionNameType.COLUMN, + DimensionNameType.ROW }; /** @@ -426,15 +428,15 @@ public class GridExtent implements GridEnvelope, LenientComparable, Serializable * @since 1.5 */ public static Optional<DimensionNameType> typeFromAxis(final CoordinateSystemAxis axis) { - return Optional.ofNullable(AXIS_DIRECTIONS.get(AxisDirections.absolute(axis.getDirection()))); + return Optional.ofNullable(AXIS_DIRECTIONS.get(axis.getDirection())); } /** * Infers the axis types from the given coordinate reference system. - * This method is the converse of {@link GridExtentCRS}. + * This method is the converse of {@link GridCRSBuilder}. * * @param crs the coordinate reference system, or {@code null}. - * @param dimension number of name type to infer. Shall not be greater than the CRS dimension. + * @param dimension number of name types to infer. Shall not be greater than the <abbr>CRS</abbr> dimension. * @return axis types, or {@code null} if no axis were recognized. */ static DimensionNameType[] typeFromAxes(final CoordinateReferenceSystem crs, final int dimension) { @@ -442,7 +444,7 @@ public class GridExtent implements GridEnvelope, LenientComparable, Serializable if (crs != null) { final CoordinateSystem cs = crs.getCoordinateSystem(); for (int i=0; i<dimension; i++) { - final DimensionNameType type = AXIS_DIRECTIONS.get(AxisDirections.absolute(cs.getAxis(i).getDirection())); + final DimensionNameType type = AXIS_DIRECTIONS.get(cs.getAxis(i).getDirection()); if (type != null) { if (axisTypes == null) { axisTypes = new DimensionNameType[dimension]; @@ -1257,7 +1259,7 @@ public class GridExtent implements GridEnvelope, LenientComparable, Serializable final GeneralEnvelope envelope = toEnvelope(cornerToCRS, false, cornerToCRS, null); final Matrix gridToCRS = MathTransforms.getMatrix(cornerToCRS); if (gridToCRS != null && Matrices.isAffine(gridToCRS)) try { - envelope.setCoordinateReferenceSystem(GridExtentCRS.forExtentAlone(gridToCRS, getAxisTypes())); + GridCRSBuilder.forExtentAlone(gridToCRS, getAxisTypes()).ifPresent(envelope::setCoordinateReferenceSystem); } catch (FactoryException e) { throw new TransformException(e); } diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridExtentCRS.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridExtentCRS.java deleted file mode 100644 index 32b0f20527..0000000000 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridExtentCRS.java +++ /dev/null @@ -1,413 +0,0 @@ -/* - * 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.internal.shared.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; - -// Specific to the geoapi-3.1 and geoapi-4.0 branches: -import org.opengis.referencing.ObjectDomain; - - -/** - * 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(ObjectDomain.SCOPE_KEY, SCOPE); - gg.getGeographicExtent().ifPresent((domain) -> { - properties.put(ObjectDomain.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 = AxisDirection.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 = AxisDirection.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, AxisDirection.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); - } -} diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridGeometry.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridGeometry.java index c47e30cf16..6d8ace32d5 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridGeometry.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridGeometry.java @@ -37,7 +37,6 @@ import org.opengis.referencing.operation.Matrix; import org.opengis.referencing.operation.MathTransform; import org.opengis.referencing.operation.TransformException; import org.opengis.referencing.operation.CoordinateOperation; -import org.opengis.referencing.operation.NoninvertibleTransformException; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.crs.DerivedCRS; import org.opengis.referencing.cs.CoordinateSystem; @@ -60,6 +59,7 @@ import org.apache.sis.referencing.operation.matrix.MatrixSIS; import org.apache.sis.referencing.operation.transform.MathTransforms; import org.apache.sis.referencing.operation.transform.LinearTransform; import org.apache.sis.referencing.operation.transform.PassThroughTransform; +import org.apache.sis.referencing.factory.InvalidGeodeticParameterException; import org.apache.sis.referencing.internal.shared.ExtendedPrecisionMatrix; import org.apache.sis.referencing.internal.shared.DirectPositionView; import org.apache.sis.referencing.internal.shared.TemporalAccessor; @@ -1866,21 +1866,50 @@ public class GridGeometry implements LenientComparable, Serializable { * @param name name of the CRS to create. * @param anchor the cell part to map (center or corner). * @return a derived CRS for coordinates (cell indices) associated to the grid extent. - * @throws IncompleteGridGeometryException if the CRS, grid extent or "grid to CRS" transform is missing. + * @throws IncompleteGridGeometryException if the CRS or "grid to CRS" transform is missing. * * @since 1.3 + * + * @deprecated Replaced by the more generic {@link #createGridCRS(Identifier, PixelInCell)} method. */ + @Deprecated(since = "1.7", forRemoval = true) public DerivedCRS createImageCRS(final String name, final PixelInCell anchor) { ArgumentChecks.ensureNonEmpty("name", name); + final var id = new org.apache.sis.referencing.ImmutableIdentifier(null, null, name); try { - return GridExtentCRS.forCoverage(name, this, anchor, null); + // Note: the `true` boolean argument can be removed after the removal of this method. + final CoordinateReferenceSystem crs = new GridCRSBuilder(this, anchor, id, true, null).forCoverage(); + return (DerivedCRS) org.apache.sis.referencing.CRS.getSingleComponents(crs).get(0); } catch (FactoryException e) { throw new BackingStoreException(e); - } catch (NoninvertibleTransformException e) { - throw new IllegalStateException(e); } } + /** + * Creates a coordinate reference system for cell indices in the grid. + * If the cell indices can be related to the real world <abbr>CRS</abbr> + * by the {@linkplain #getGridToCRS grid to <abbr>CRS</abbr>} transform, + * then this method creates {@link DerivedCRS} instances derived from the + * {@link #getCoordinateReferenceSystem() <abbr>CRS</abbr> of this grid}. + * This strategy makes possible to use the returned <abbr>CRS</abbr> in a chain of operations + * with (for example) {@link org.apache.sis.referencing.CRS#findOperation CRS.findOperation(…)}. + * Otherwise (if there is no grid to <abbr>CRS</abbr> transform or no real-world <abbr>CRS</abbr>), + * this method creates {@link org.opengis.referencing.crs.EngineeringCRS} instances. + * + * @param name name of the <abbr>CRS</abbr> to create. + * @param anchor the cell part to map (center or corner). + * @return a derived, engineering or compound <abbr>CRS</abbr> for cell indices associated to the grid extent. + * @throws InvalidGeodeticParameterException if characteristics of this grid geometry disallow this operation. + * @throws FactoryException if another error occurred during the use of a referencing factory. + * + * @since 1.7 + */ + public CoordinateReferenceSystem createGridCRS(final Identifier name, final PixelInCell anchor) throws FactoryException { + ArgumentChecks.ensureNonNull("name", name); + ArgumentChecks.ensureNonNull("anchor", anchor); + return new GridCRSBuilder(this, anchor, name, false, null).forCoverage(); + } + /** * Returns a coordinate operation for transforming coordinates from the <abbr>CRS</abbr> of this grid geometry * to the given <abbr>CRS</abbr>. The {@linkplain #getGeographicExtent() geographic bounding box} of this grid diff --git a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/GridGeometryTest.java b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/GridGeometryTest.java index 0f7c2a0c69..7bb3a95c42 100644 --- a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/GridGeometryTest.java +++ b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/GridGeometryTest.java @@ -17,16 +17,22 @@ package org.apache.sis.coverage.grid; import java.util.Set; +import java.util.List; import org.opengis.util.FactoryException; import org.opengis.geometry.Envelope; +import org.opengis.metadata.Identifier; import org.opengis.metadata.spatial.DimensionNameType; import org.opengis.referencing.cs.AxisDirection; +import org.opengis.referencing.crs.SingleCRS; import org.opengis.referencing.crs.DerivedCRS; +import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.operation.Matrix; import org.opengis.referencing.operation.MathTransform; import org.opengis.referencing.operation.TransformException; import org.opengis.referencing.operation.NoninvertibleTransformException; import org.apache.sis.geometry.GeneralEnvelope; +import org.apache.sis.referencing.CRS; +import org.apache.sis.referencing.ImmutableIdentifier; import org.apache.sis.referencing.operation.MissingSourceDimensionsException; import org.apache.sis.referencing.operation.matrix.Matrix2; import org.apache.sis.referencing.operation.matrix.Matrix3; @@ -808,10 +814,12 @@ public final class GridGeometryTest extends TestCase { } /** - * Tests {@link GridGeometry#createImageCRS(String, PixelInCell)}. + * Tests {@link GridGeometry#createGridCRS(Identifier, PixelInCell)}. + * + * @throws FactoryException if the <abbr>CRS</abbr> cannot be built. */ @Test - public void testCreateImageCRS() { + public void testCreateGridCRS() throws FactoryException { final var gg = new GridGeometry( new GridExtent(null, null, new long[] {17, 10, 4}, true), PixelInCell.CELL_CENTER, @@ -822,14 +830,27 @@ public final class GridGeometryTest extends TestCase { 0, 0, 0, 1)), HardCodedCRS.WGS84_WITH_TIME); - final DerivedCRS crs = gg.createImageCRS("Horizontal part", PixelInCell.CELL_CENTER); - assertEquals("Horizontal part", crs.getName().getCode()); - assertSame(HardCodedCRS.WGS84, crs.getBaseCRS()); + final Identifier name = new ImmutableIdentifier(null, null, "Tested grid CRS"); + final CoordinateReferenceSystem crs = gg.createGridCRS(name, PixelInCell.CELL_CENTER); + assertSame(name, crs.getName()); + + final List<SingleCRS> components = CRS.getSingleComponents(crs); + assertEquals(2, components.size()); + + final var horizontal = assertInstanceOf(DerivedCRS.class, components.get(0)); + assertSame(HardCodedCRS.WGS84, horizontal.getBaseCRS()); assertMatrixEquals( new Matrix3(1, 0, 7, // Opposite sign because this is the inverse transform. 0, -1, 50, // Opposite sign cancelled by -1 scale factor. 0, 0, 1), - crs.getConversionFromBase().getMathTransform(), + horizontal.getConversionFromBase().getMathTransform(), + "CRS to grid"); + + final var temporal = assertInstanceOf(DerivedCRS.class, components.get(1)); + assertSame(HardCodedCRS.TIME, temporal.getBaseCRS()); + assertMatrixEquals( + new Matrix2(0.125, -2.5, 0, 1), + temporal.getConversionFromBase().getMathTransform(), "CRS to grid"); } diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/geometry/Envelopes.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/geometry/Envelopes.java index 85cdcf3162..96f352fd07 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/geometry/Envelopes.java +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/geometry/Envelopes.java @@ -191,7 +191,7 @@ public final class Envelopes { * or if the CRS of all envelopes is {@code null}, then the {@linkplain GeneralEnvelope#add(Envelope) * union is computed} without transforming any envelope. Otherwise, all envelopes are transformed * to a {@linkplain CRS#suggestCommonTarget common CRS} before union is computed. - * The CRS of the returned envelope may different than the CRS of all given envelopes. + * The CRS of the returned envelope may be different than the CRS of all given envelopes. * * @param envelopes the envelopes for which to compute union. Null elements are ignored. * @return union of given envelopes, or {@code null} if the given array does not contain non-null elements. diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/IdentifiedObjects.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/IdentifiedObjects.java index 2c4d11edd9..7bfa858c13 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/IdentifiedObjects.java +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/IdentifiedObjects.java @@ -329,9 +329,9 @@ public final class IdentifiedObjects { * <li><code>object.{@linkplain AbstractIdentifiedObject#getIdentifiers() getIdentifiers()}</code> in iteration order</li> * </ul> * - * This method is can be used for fetching a more human-friendly identifier than the numerical values - * typically returned by {@link IdentifiedObject#getIdentifiers()}. However, the returned value is not - * guaranteed to be unique. + * This method can be used for fetching a more human-friendly identifier than the numerical values + * typically returned by {@link IdentifiedObject#getIdentifiers()}. + * However, the returned value is not guaranteed to be unique. * * @param object the identified object, or {@code null}. * @return the first name, alias or identifier which is a valid Unicode identifier, or {@code null} if none. diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/AbstractCS.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/AbstractCS.java index ba1bc7fc43..aeb4c3dd9a 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/AbstractCS.java +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/AbstractCS.java @@ -305,7 +305,7 @@ public class AbstractCS extends AbstractIdentifiedObject implements CoordinateSy * Returns the axes of the given coordinate system. */ private static CoordinateSystemAxis[] getAxes(final CoordinateSystem cs) { - final CoordinateSystemAxis[] axes = new CoordinateSystemAxis[cs.getDimension()]; + final var axes = new CoordinateSystemAxis[cs.getDimension()]; for (int i=0; i<axes.length; i++) { axes[i] = cs.getAxis(i); } diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/DefaultAffineCS.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/DefaultAffineCS.java index c8d0bdab06..8a6a5c82c1 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/DefaultAffineCS.java +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/DefaultAffineCS.java @@ -183,7 +183,7 @@ public class DefaultAffineCS extends AbstractCS implements AffineCS { if (!AxisDirections.isSpatialOrUserDefined(direction, true)) { return INVALID_DIRECTION; } - if (!Units.isLinear(unit) && !Units.UNITY.equals(unit) && !Units.PIXEL.equals(unit)) { + if (!(Units.isLinear(unit) || Units.UNITY.equals(unit) || Units.PIXEL.equals(unit))) { return INVALID_UNIT; } return VALID; diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/DefaultCylindricalCS.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/DefaultCylindricalCS.java index f8b7c7b84d..ba462fa625 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/DefaultCylindricalCS.java +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/DefaultCylindricalCS.java @@ -161,7 +161,7 @@ public class DefaultCylindricalCS extends AbstractCS implements CylindricalCS { if (!AxisDirections.isSpatialOrUserDefined(direction, false)) { return INVALID_DIRECTION; } - if (!Units.isAngular(unit) && !Units.isLinear(unit)) { + if (!(Units.isAngular(unit) || Units.isLinear(unit))) { return INVALID_UNIT; } return VALID; diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/DefaultLinearCS.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/DefaultLinearCS.java index dfe8a0fd62..4ae885bf29 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/DefaultLinearCS.java +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/DefaultLinearCS.java @@ -152,7 +152,7 @@ public class DefaultLinearCS extends AbstractCS implements LinearCS { if (!AxisDirections.isSpatialOrUserDefined(direction, false)) { return INVALID_DIRECTION; } - if (!Units.isLinear(unit) && !Units.UNITY.equals(unit)) { + if (!(Units.isLinear(unit) || Units.UNITY.equals(unit))) { return INVALID_UNIT; } return VALID; diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/DefaultPolarCS.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/DefaultPolarCS.java index 4eedd6720c..7dcf0880eb 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/DefaultPolarCS.java +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/DefaultPolarCS.java @@ -159,7 +159,7 @@ public class DefaultPolarCS extends AbstractCS implements PolarCS { if (!AxisDirections.isSpatialOrUserDefined(direction, false)) { return INVALID_DIRECTION; } - if (!Units.isLinear(unit) && !Units.isAngular(unit)) { + if (!(Units.isLinear(unit) || Units.isAngular(unit))) { return INVALID_UNIT; } return VALID; diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/DefaultSphericalCS.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/DefaultSphericalCS.java index 139e75ab5a..1ea92219a4 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/DefaultSphericalCS.java +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/DefaultSphericalCS.java @@ -184,7 +184,7 @@ public class DefaultSphericalCS extends AbstractCS implements SphericalCS { if (!AxisDirections.isSpatialOrUserDefined(direction, false)) { return INVALID_DIRECTION; } - if (!Units.isAngular(unit) && !Units.isLinear(unit)) { + if (!(Units.isAngular(unit) || Units.isLinear(unit))) { return INVALID_UNIT; } return VALID; diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/DefaultTimeCS.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/DefaultTimeCS.java index f4a44d6a5c..1b3da332c9 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/DefaultTimeCS.java +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/DefaultTimeCS.java @@ -50,7 +50,7 @@ import org.opengis.referencing.cs.CoordinateDataType; * constants. * * @author Martin Desruisseaux (IRD, Geomatys) - * @version 1.5 + * @version 1.7 * * @see org.apache.sis.referencing.crs.DefaultTemporalCRS * @see org.apache.sis.referencing.datum.DefaultTemporalDatum @@ -149,14 +149,18 @@ public class DefaultTimeCS extends AbstractCS implements TimeCS { * Returns {@code VALID} if the given argument values are allowed for this coordinate system, * or an {@code INVALID_*} error code otherwise. This method is invoked at construction time. * The current implementation accepts only temporal directions (i.e. {@link AxisDirection#FUTURE} - * and {@link AxisDirection#PAST}) and units compatible with {@link Units#SECOND}. + * and {@link AxisDirection#PAST}). + * + * @see #getCoordinateType() */ @Override final int validateAxis(final AxisDirection direction, final Unit<?> unit) { if (!AxisDirections.isTemporal(direction)) { return INVALID_DIRECTION; } - if (!Units.isTemporal(unit)) { + // ISO 19111 allows count of something. SIS uses that for cell index. + // TODO: A null unit should be valid and means to use `java.time`. + if (!(Units.isTemporal(unit) || Units.UNITY.equals(unit))) { return INVALID_UNIT; } return VALID; @@ -179,8 +183,7 @@ public class DefaultTimeCS extends AbstractCS implements TimeCS { } /** - * Returns the type (measure, integer or data-time) of coordinate values. - * The current implementation supports only {@link CoordinateDataType#MEASURE}. + * Returns the type (measure, integer or date-time) of coordinate values. * * @return the type of coordinate values. * @@ -188,7 +191,14 @@ public class DefaultTimeCS extends AbstractCS implements TimeCS { */ @Override public CoordinateDataType getCoordinateType() { - return CoordinateDataType.MEASURE; + final Unit<?> unit = getAxis(0).getUnit(); + if (unit == null) { + return CoordinateDataType.DATE_TIME; + } else if (unit.equals(Units.UNITY)) { + return CoordinateDataType.INTEGER; + } else { + return CoordinateDataType.MEASURE; + } } /** diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/DefaultVerticalCS.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/DefaultVerticalCS.java index 713ae3e1bb..fb64e261c4 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/DefaultVerticalCS.java +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/DefaultVerticalCS.java @@ -156,7 +156,7 @@ public class DefaultVerticalCS extends AbstractCS implements VerticalCS { /** * Returns {@code VALID} if the given argument values are allowed for this coordinate system, * or an {@code INVALID_*} error code otherwise. This method is invoked at construction time. - * The current implementation accepts only temporal directions (i.e. {@link AxisDirection#UP} + * The current implementation accepts only vertical directions (i.e. {@link AxisDirection#UP} * and {@link AxisDirection#DOWN}). */ @Override diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/package-info.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/package-info.java index ba79901991..9f03ef3e1b 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/package-info.java +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/package-info.java @@ -33,7 +33,7 @@ * and units between two coordinate systems, or filtering axes. * * @author Martin Desruisseaux (IRD, Geomatys) - * @version 1.5 + * @version 1.7 * @since 0.4 */ @XmlSchema(location = "http://schemas.opengis.net/gml/3.2.1/coordinateSystems.xsd", diff --git a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/referencing/AuthorityCodes.java b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/referencing/AuthorityCodes.java index 8d9242d824..d0c0bc0062 100644 --- a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/referencing/AuthorityCodes.java +++ b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/referencing/AuthorityCodes.java @@ -49,8 +49,8 @@ import static org.apache.sis.gui.internal.LogHandler.LOGGER; /** - * A list of authority codes (usually for CRS) which fetch code values in a background thread - * and CRS names only when needed. + * A list of authority codes (usually for <abbr>CRS</abbr>) for which to fetch code values in a background thread. + * The <abbr>CRS</abbr> names are fetched only when needed. * * @todo {@link org.apache.sis.referencing.factory.sql.EPSGDataAccess} internally uses a {@link java.util.Map} * from codes to descriptions. We could open an access to this map for a little bit more efficiency. @@ -64,7 +64,7 @@ final class AuthorityCodes extends ObservableListBase<Code> { /** * Delay in nanoseconds before to refresh the list with new content. - * Data will be transferred from background threads to JavaFX threads every time this delay is elapsed. + * Data will be transferred from background threads to JavaFX thread every time this delay is elapsed. * The delay value is a compromise between fast user experience and giving enough time for doing a few * large data transfers instead of many small data transfers. */ @@ -235,6 +235,7 @@ final class AuthorityCodes extends ObservableListBase<Code> final ListIterator<Object> it = codes.listIterator(); while (it.hasNext()) { final Object value = it.next(); + @SuppressWarnings("element-type-mismatch") final String name = result.names.remove(value); if (name != null) { final int i = it.previousIndex(); @@ -381,7 +382,7 @@ final class AuthorityCodes extends ObservableListBase<Code> * @param factory value of {@link #getFactory()}. * @return the names of CRS authority codes submitted to {@link #requestName(Code)}, or {@code null} if none. */ - private Map<Code,String> processNameRequests(final CRSAuthorityFactory factory) { + private Map<Code, String> processNameRequests(final CRSAuthorityFactory factory) { final Code[] snapshot; synchronized (toDescribe) { final int size = toDescribe.size(); @@ -389,7 +390,7 @@ final class AuthorityCodes extends ObservableListBase<Code> snapshot = toDescribe.toArray(new Code[size]); toDescribe.clear(); } - final Map<Code,String> updated = new IdentityHashMap<>(snapshot.length); + final var updated = new IdentityHashMap<Code, String>(snapshot.length); for (final Code code : snapshot) { String text; try { @@ -434,7 +435,7 @@ final class AuthorityCodes extends ObservableListBase<Code> while (it.hasNext()) { codes.add(it.next()); if (System.nanoTime() - lastTime > REFRESH_DELAY) { - final PartialResult p = new PartialResult(codes.toArray(), processNameRequests(factory)); + final var p = new PartialResult(codes.toArray(), processNameRequests(factory)); codes.clear(); Platform.runLater(() -> update(p)); lastTime = System.nanoTime(); @@ -479,7 +480,7 @@ final class AuthorityCodes extends ObservableListBase<Code> final Throwable e = getException(); errorOccurred(e); if (loadCodes) { - final Code code = new Code(Vocabulary.forLocale(locale).getString(Vocabulary.Keys.Errors)); + final var code = new Code(Vocabulary.forLocale(locale).getString(Vocabulary.Keys.Errors)); String message = Exceptions.getLocalizedMessage(e, locale); if (message == null) { message = Classes.getShortClassName(e); diff --git a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/referencing/CRSChooser.java b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/referencing/CRSChooser.java index 1ca67d877c..5c98a942b8 100644 --- a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/referencing/CRSChooser.java +++ b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/referencing/CRSChooser.java @@ -166,8 +166,8 @@ public class CRSChooser extends Dialog<CoordinateReferenceSystem> { * Columns to show in CRS table. First column is typically EPSG codes and second * column is the CRS descriptions. The content is loaded in a background thread. */ - final TableColumn<Code,Code> codes = new TableColumn<>(vocabulary.getString(Vocabulary.Keys.Code)); - final TableColumn<Code,String> names = new TableColumn<>(vocabulary.getString(Vocabulary.Keys.Name)); + final var codes = new TableColumn<Code, Code> (vocabulary.getString(Vocabulary.Keys.Code)); + final var names = new TableColumn<Code, String>(vocabulary.getString(Vocabulary.Keys.Name)); names.setCellValueFactory(codeList); codes.setCellValueFactory(IdentityValueFactory.instance()); codes.setCellFactory(Code.Cell::new); @@ -199,7 +199,7 @@ public class CRSChooser extends Dialog<CoordinateReferenceSystem> { * Button for showing the CRS description in Well Known Text (WKT) format. * The button is enabled only if a row in the table is selected. */ - final ToggleButton infoButton = new ToggleButton("\uD83D\uDDB9"); // Unicode U+1F5B9: Document With Text. + final var infoButton = new ToggleButton("\uD83D\uDDB9"); // Unicode U+1F5B9: Document With Text. table.getSelectionModel().selectedItemProperty().addListener((e,o,n) -> { infoButton.setDisable(n == null); updateSummary(n); diff --git a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/referencing/MenuSync.java b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/referencing/MenuSync.java index 36236ed27d..63c147bcf2 100644 --- a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/referencing/MenuSync.java +++ b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/referencing/MenuSync.java @@ -31,7 +31,6 @@ import javafx.scene.control.RadioMenuItem; import javafx.scene.control.SeparatorMenuItem; import javafx.scene.control.ToggleGroup; import org.opengis.referencing.ReferenceSystem; -import org.opengis.referencing.crs.DerivedCRS; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.apache.sis.referencing.CRS; import org.apache.sis.referencing.IdentifiedObjects; @@ -80,7 +79,7 @@ final class MenuSync extends SimpleObjectProperty<ReferenceSystem> implements Ev * The content of this list depends on the grid coverages shown in the widget. * This is {@code null} if that sub-menu is omitted. */ - private final List<DerivedCRS> cellIndicesSystems; + private final List<CoordinateReferenceSystem> cellIndicesSystems; /** * The list of menu items to keep up-to-date with {@link #recentSystems}. @@ -125,7 +124,7 @@ final class MenuSync extends SimpleObjectProperty<ReferenceSystem> implements Ev * @param bean the menu to keep synchronized with the list of reference systems. * @param action a wrapper over the user-specified action to execute when a reference system is selected. */ - MenuSync(final List<ReferenceSystem> systems, final boolean byIds, final List<DerivedCRS> derived, + MenuSync(final List<ReferenceSystem> systems, final boolean byIds, final List<CoordinateReferenceSystem> derived, final Menu bean, final RecentReferenceSystems.SelectionListener action) { super(bean, "value"); @@ -230,7 +229,7 @@ final class MenuSync extends SimpleObjectProperty<ReferenceSystem> implements Ev private void updateCellIndicesMenus(final Locale locale) { final int n = cellIndicesSystems.size(); for (int i=0; i<n; i++) { - final DerivedCRS crs = cellIndicesSystems.get(i); + final CoordinateReferenceSystem crs = cellIndicesSystems.get(i); final RadioMenuItem item; if (i < cellIndicesMenus.size()) { item = (RadioMenuItem) cellIndicesMenus.get(i); diff --git a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/referencing/RecentReferenceSystems.java b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/referencing/RecentReferenceSystems.java index 76eac51ae7..a0925bc82e 100644 --- a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/referencing/RecentReferenceSystems.java +++ b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/referencing/RecentReferenceSystems.java @@ -39,13 +39,13 @@ import org.opengis.util.FactoryException; import org.opengis.geometry.Envelope; import org.opengis.referencing.ReferenceSystem; import org.opengis.referencing.IdentifiedObject; -import org.opengis.referencing.crs.DerivedCRS; import org.opengis.referencing.crs.CRSAuthorityFactory; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.operation.TransformException; import org.apache.sis.geometry.Envelopes; import org.apache.sis.geometry.ImmutableEnvelope; import org.apache.sis.referencing.IdentifiedObjects; +import org.apache.sis.referencing.ImmutableIdentifier; import org.apache.sis.referencing.factory.GeodeticAuthorityFactory; import org.apache.sis.referencing.factory.IdentifiedObjectFinder; import org.apache.sis.referencing.gazetteer.MilitaryGridReferenceSystem; @@ -211,26 +211,18 @@ public class RecentReferenceSystems { * instances and duplicated values removed. This is the list given to JavaFX controls that we build. * This list includes {@link #OTHER} as its last item. * - * <p>This list is initially null and created only when first needed. After the list has been created, - * this reference is never modified. As long as the reference is null, we can skip the synchronization - * of this list content with the {@link #systemsOrCodes} content when the latter changed. Because that - * synchronization may involve accesses to the EPSG database, it is potentially costly.</p> - * * @see #getReferenceSystems(boolean) */ - private ObservableList<ReferenceSystem> referenceSystems; + private final ObservableList<ReferenceSystem> referenceSystems; /** * A view of {@link #referenceSystems} with only items that are instances of {@link CoordinateReferenceSystem}. * This list includes also {@link #OTHER} as its last item. This list is used for menus shown in contexts where * identifiers cannot be used, for example for selecting the CRS to use for displaying a map. * - * <p>This list is lazily created when first needed, - * because it depends on {@link #referenceSystems} which is itself lazily created.</p> - * * @see #getReferenceSystems(boolean) */ - private ObservableList<ReferenceSystem> coordinateReferenceSystems; + private final ObservableList<ReferenceSystem> coordinateReferenceSystems; /** * A filtered view of {@link #referenceSystems} without the {@link #OTHER} item. @@ -242,12 +234,9 @@ public class RecentReferenceSystems { * handled in a special way by {@link ObjectStringConverter} for making the "Other…" item present in the * list of choices. But since {@link #OTHER} is not a real CRS, we want to hide that trick to users. * - * <p>This list is lazily created when first needed, - * because it depends on {@link #referenceSystems} which is itself lazily created.</p> - * * @see #getItems() */ - private ObservableList<ReferenceSystem> publicItemList; + private final ObservableList<ReferenceSystem> publicItemList; /** * Coordinate reference systems used for computing cell indices of grid coverages. @@ -256,7 +245,7 @@ public class RecentReferenceSystems { * * @see #setGridReferencing(boolean, Map) */ - private final List<DerivedCRS> cellIndiceSystems; + private final List<CoordinateReferenceSystem> cellIndiceSystems; /** * {@code true} if the {@link #referenceSystems} list needs to be rebuilt from {@link #systemsOrCodes} content. @@ -310,6 +299,11 @@ public class RecentReferenceSystems { geographicAOI = Utils.toGeographic(RecentReferenceSystems.class, "areaOfInterest", n); listModified(); }); + referenceSystems = FXCollections.observableArrayList(); + publicItemList = new FilteredList<>(referenceSystems, Objects::nonNull); + coordinateReferenceSystems = new FilteredList<>(referenceSystems, (ReferenceSystem system) -> { + return (system == OTHER) || (system instanceof CoordinateReferenceSystem); + }); } /** @@ -344,33 +338,34 @@ public class RecentReferenceSystems { * Fetch or compute information needed, but without modifying the state of this object yet. * All assignments to `this` should be done inside the `try … finally` block. */ - int countEnv = 0; int countCRS = 0; int countCIR = 0; - final Envelope[] envelopes = new Envelope[geometries.size()]; - final DerivedCRS[] derived = new DerivedCRS[geometries.size()]; - final var alt = new CoordinateReferenceSystem[Math.max(derived.length - 1, 0)]; - CoordinateReferenceSystem firstCRS = null; - for (final Map.Entry<String,GridGeometry> entry : geometries.entrySet()) { + final var refsys = new CoordinateReferenceSystem[geometries.size()]; + final var derived = new CoordinateReferenceSystem[refsys.length]; + final var envelopes = new Envelope[refsys.length]; + for (final Map.Entry<String, GridGeometry> entry : geometries.entrySet()) { final GridGeometry gg = entry.getValue(); - if (gg.isDefined(GridGeometry.ENVELOPE)) { - envelopes[countEnv++] = gg.getEnvelope(); - } if (gg.isDefined(GridGeometry.CRS)) { - final CoordinateReferenceSystem crs = gg.getCoordinateReferenceSystem(); - if (firstCRS == null) { - firstCRS = crs; - } else { - alt[countCRS++] = crs; - } - if (gg.isDefined(GridGeometry.GRID_TO_CRS | GridGeometry.EXTENT)) { - derived[countCIR++] = gg.createImageCRS(entry.getKey(), PixelInCell.CELL_CENTER); + if (gg.isDefined(GridGeometry.ENVELOPE)) { + envelopes[countCRS] = gg.getEnvelope(); } + refsys[countCRS++] = gg.getCoordinateReferenceSystem(); } + try { + final var name = new ImmutableIdentifier(null, null, entry.getKey()); + derived[countCIR] = gg.createGridCRS(name, PixelInCell.CELL_CENTER); + countCIR++; // Increment only if above line was successful. + } catch (FactoryException e) { + errorOccurred(e); + } + } + if (countCRS == 0 && countCIR != 0) { + refsys[0] = derived[0]; + countCRS = 1; } Envelope union; try { - union = Envelopes.union(envelopes); // No need to trim null elements. + union = Envelopes.union(envelopes); // Null elements are ignored. } catch (TransformException e) { errorOccurred("setGridReferencing", e); union = null; @@ -381,24 +376,19 @@ public class RecentReferenceSystems { * in order to have only one call to `filterReferenceSystems(…)`. */ final Envelope aoi = union; // Because lambda functions want a final variable. - final CoordinateReferenceSystem preferred = firstCRS; - final List<DerivedCRS> cellCRS = Containers.viewAsUnmodifiableList(derived, 0, countCIR); + final List<CoordinateReferenceSystem> cellCRS = Containers.viewAsUnmodifiableList(derived, 0, countCIR); final int stamp = modificationCount.incrementAndGet(); Platform.runLater(() -> { if (modificationCount.get() == stamp) { - final ObservableList<ReferenceSystem> savedReferenceSystemList = referenceSystems; - try { - referenceSystems = null; - if (preferred != null) { - setPreferred(replaceByAuthoritativeDefinition, preferred); - addAlternatives(replaceByAuthoritativeDefinition, alt); // No need to trim null elements. - cellIndiceSystems.clear(); - cellIndiceSystems.addAll(cellCRS); - } - areaOfInterest.set(aoi); - } finally { - referenceSystems = savedReferenceSystemList; + final CoordinateReferenceSystem preferred = refsys[0]; + if (preferred != null) { + refsys[0] = null; + setPreferred(replaceByAuthoritativeDefinition, preferred); + addAlternatives(replaceByAuthoritativeDefinition, refsys); // Null elements are ignored. + cellIndiceSystems.clear(); + cellIndiceSystems.addAll(cellCRS); } + areaOfInterest.set(aoi); listModified(); } }); @@ -560,7 +550,7 @@ public class RecentReferenceSystems { boolean noFactoryFound = false; boolean searchedFinder = false; IdentifiedObjectFinder finder = null; - for (int i=systemsOrCodes.size(); --i >= 0;) { + for (int i = systemsOrCodes.size(); --i >= 0;) { final Object item = systemsOrCodes.get(i); if (item instanceof ReferenceSystem) { continue; @@ -617,7 +607,7 @@ public class RecentReferenceSystems { * (execution time of O(N²)) but it should not be an issue if this list is short (e.g. * 20 elements). We cut the list if we reach the maximal number of systems to keep. */ - for (int i=0,j; i < (j=systemsOrCodes.size()); i++) { + for (int i=0, j; i < (j = systemsOrCodes.size()); i++) { if (i >= RecentChoices.MAXIMUM_REFERENCE_SYSTEMS) { systemsOrCodes.subList(i, j).clear(); break; @@ -673,10 +663,7 @@ public class RecentReferenceSystems { private void listModified() { synchronized (systemsOrCodes) { isModified = true; - if (referenceSystems != null) { - // ChoiceBox or Menu already created. They will observe the changes in item list. - getReferenceSystems(false); - } + getReferenceSystems(false); } } @@ -690,9 +677,6 @@ public class RecentReferenceSystems { */ @SuppressWarnings("ReturnOfCollectionOrArrayField") private ObservableList<ReferenceSystem> getReferenceSystems(final boolean filtered) { - if (referenceSystems == null) { - referenceSystems = FXCollections.observableArrayList(); - } synchronized (systemsOrCodes) { /* * Prepare a temporary list as the concatenation of all items that are currently visible in JavaFX @@ -736,22 +720,11 @@ public class RecentReferenceSystems { } } if (filtered) { - if (coordinateReferenceSystems == null) { - coordinateReferenceSystems = new FilteredList<>(referenceSystems, RecentReferenceSystems::isCRS); - } return coordinateReferenceSystems; } return referenceSystems; } - /** - * Returns {@code true} if the given reference system can be included - * in the {@link #coordinateReferenceSystems} list. - */ - private static boolean isCRS(final ReferenceSystem system) { - return (system == OTHER) || (system instanceof CoordinateReferenceSystem); - } - /** * Sets the reference systems to the given content. The given list is often similar to current content, * for example with only a reference system that moved to a different index. This method compares the @@ -945,9 +918,6 @@ public class RecentReferenceSystems { */ @SuppressWarnings("ReturnOfCollectionOrArrayField") public ObservableList<ReferenceSystem> getItems() { - if (publicItemList == null) { - publicItemList = new FilteredList<>(getReferenceSystems(false), Objects::nonNull); - } return publicItemList; } @@ -1062,7 +1032,7 @@ next: for (int i=0; i<count; i++) { public Menu createMenuItems(final boolean filtered, final ChangeListener<ReferenceSystem> action) { ArgumentChecks.ensureNonNull("action", action); final List<ReferenceSystem> main = getReferenceSystems(filtered); - final List<DerivedCRS> derived = (filtered) ? null : cellIndiceSystems; + final List<CoordinateReferenceSystem> derived = (filtered) ? null : cellIndiceSystems; final var menu = new Menu(Vocabulary.forLocale(locale).getString(Vocabulary.Keys.ReferenceSystem)); final var property = new MenuSync(main, !filtered, derived, menu, new SelectionListener(action)); menu.getProperties().put(SELECTED_ITEM_KEY, property);
